iOS 2021 面试前的准备(总结各知识点方便面试前快速复习使用)(二)

1,979 阅读53分钟

 博主前期通读了 Apple 的五份源码 objc4-781libdispatch-1173.40.5CF-1151.16libmalloc-283.100.6libclosure-74 基本对 iOS 的大部分底层原理都有了一个基础的认知,然后算法部分的话是专注刷了两遍 《剑指 Offer》(在 IDE 里可以完成默写,完全手写的话可能还需要一些练习)。那么既然是面试肯定免不了要刷题,题目的话就从网络搜集各位大佬面试时的题目以及本人面试时被问到的题目,然后试着从自己的理解上给题目作出解答,如有错误的地方还望大家进行指正。

11. 进程和线程、并行和并发、同步和异步的理解。

 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存,是操作系统结构的基础。

 进程是正在运行的程序的实例,当一个程序进入内存运行时,即成为了一个进程,例如在 iOS 中,一个 App 的启动就是开启一个进程。程序是指令、数据及其组织形式的描述。

 在当代面向线程设计的计算机结构中,进程是线程的容器。

 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。

 进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

 进程切换:让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针 PCPC 寄存器),于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程的上下文是存储在进程的私有堆栈中的。

 线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

 在程序被运行后,系统首先要做的就是为该程序进程建立一个默认线程(在 iOS 中 App 启动会默认为我们开启一条主线程),然后程序可以根据需要自行添加或删除相关的线程。

 通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小的多,能更高效的提高系统内多个程序间并发执行的程度。

 同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

 在多核或多 CPU,或支持 Hyper-threadingCPU 上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单 CPU 单核的计算机上,使用多线程技术,也可以把进程中负责 I/O 处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的 workhorse 线程执行密集计算,从而提高了程序的执行效率。

 进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一个地址空间。

 与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。

 通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小的多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。

 线程与进程的区别可以归纳为以下 4 点:

  1. 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  2. 通信:进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信--需要进程同步和互斥手段的辅助,以保证数据的一致性。
  3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
  4. 在多线程 OS 中,进程不是一个可执行的实体。

 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程不拥有系统资源,只有运行必须的一些数据结构;它与父进程的其它线程共享该进程所拥有的全部资源。线程可以创建和撤消线程,从而实现程序的并发执行。一般,线程具有就绪、阻塞和运行三种基本状态。

 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。

 线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络 sockets 等的数量。 例如,线程数一般取 CPU 数量 +2 比较合适,线程数过多会导致额外的线程切换开销。

 任务调度以执行线程的常见方法是使用同步队列,称作任务队列。池中的线程等待队列中的任务,并把执行完的任务放入完成队列中。

 并发:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

 并发当有多个线程在操作时,如果系统只有一个 CPU,则它根本不可能真正同时进行一个以上的线程,它只能把 CPU 运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。

 并行:当系统有一个以上 CPU 时,则线程的操作有可能非并发。当一个 CPU 执行一个线程时,另一个 CPU 可以执行另一个线程,两个线程互不抢占 CPU 资源,可以同时进行,这种方式我们称之为并行(Parallel)。

 区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。

 任务:每次执行的一段代码,比如下载一张图片,触发一个网络请求。

 队列:队列是用来组织任务的,一个队列包含多个任务。

 队列是对任务的描述,它可以包含多个任务,这是应用层的一种描述。线程是系统级的调度单位,它是更底层的描述。一个队列(并行队列)的多个任务可能会被分配到多个线程执行。

 在 iOS 中主线程是一个线程,主队列是指主线程上的任务组织形式。

 主队列只会在主线程执行,但主线程上执行的不一定就是主队列,还有可能是别的同步队列。同步操作不会开辟新的线程,所以当你自定义一个同步的串行或者并行队列时都是还在主线程执行。

 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。

 异步的另外一种含义是计算机多线程的异步处理。与同步处理相对,异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程。

 同步 sync:只能在当前线程按先后顺序依次执行任务,不具备开启新线程的能力。(阻塞当前线程,等待任务执行完成)

 异步 async:在新的线程中执行任务,具备开启新线程的能力。(不阻塞当前线程,不等待任务执行完成)

 参考链接🔗🔗:


12. iOS 中的线程锁都有哪些?

 锁是常用的同步工具,一段代码段在同一个时间只能允许被有限个线程访问。比如一个线程 A 进入需要保护的代码之前添加简单的互斥锁,另一个线程 B 就无法访问这段保护代码了,只有等待前一个线程 A 执行完被保护的代码后解锁,B 线程才能访问被保护的代码段。

 iOS 开发中使用到的锁,包括 spinlock_t、os_unfair_lock、pthread_mutex_t、NSLock、NSRecursiveLock、NSCondition、NSConditionLock、@synchronized、dispatch_semaphore、pthread_rwlock_t。

 OSSpinLock 自旋锁,也只有加锁、解锁和尝试加锁三个方法。和 NSLock 不同的是 NSLock 请求加锁失败的话,会先轮询,但一秒后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。

 OSSpinLock 存在线程安全问题,它可能导致优先级反转问题,目前我们在任何情况下都不应该再使用它,我们可以使用 apple 在 iOS 10.0 后推出的 os_unfair_lock(iOS 10 以后 OSSpinLock 内部是使用 os_unfair_lock 实现的)。'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead

OSSpinLock lock = OS_SPINLOCK_INIT; 初始化锁,OSSpinLockLock(&lock); 获得锁(加锁),尽管锁定操作旋转,(当加锁失败时会一直处于等待状态,一直到获取到锁为止,获取到锁之前会一直阻塞当前线程)它采用各种策略来支持如果加锁成功,则关闭旋转。OSSpinLockUnlock(&lock); 解锁,OSSpinLockTry(&lock); 尝试加锁,bool 类型的返回值表示是否加锁成功,即使加锁失败也不会阻塞线程。如果锁已经被另一个线程所持有则返回 false,否则返回 true 表示加锁成功。

 自旋锁 OSSpinLock 不是一个线程安全的锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着 CPU 资源。(类似一个 while(1) 循环一样,不停的查询锁的状态,注意区分 run loop 机制,同样是阻塞,但是 run loop 是类似休眠的阻塞,不会耗费 CPU 资源,自旋锁的这种忙等机制使它相比其它锁效率更高,毕竟没有 唤醒-休眠 这些类似操作,从而能更快的处理事情。)自旋锁目前已经被废弃了,它可能会导致优先级反转。

 例如 A/B 两个线程,A 的优先级大于 B 的,我们的本意是 A 的任务优先执行,但是使用 OSSpinLock 后,如果是 B 优先访问了共享资源获得了锁并加锁,而 A 线程再去访问共享资源的时候锁就会处于忙等状态,由于 A 的优先级高它会一直占用 CPU 资源不会让出时间片,这样 B 一直不能获得 CPU 资源去执行任务,导致无法完成。

 《不再安全的 OSSpinLock》原文: 新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。 具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。 苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。 libobjc 里用的是 Mach 内核的 thread_switch() 然后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock 也不能有改动。 最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。-《不再安全的 OSSpinLock》

 os_unfair_lock 设计宗旨是用于替换 OSSpinLock,从 iOS 10 之后开始支持,跟 OSSpinLock 不同,等待 os_unfair_lock 的线程会处于休眠状态(类似 run loop 那样),不是忙等(busy-wait)。

// os_unfair_lock 是一个结构体
typedef struct os_unfair_lock_s {

uint32_t _os_unfair_lock_opaque;

} os_unfair_lock, *os_unfair_lock_t;
  1. os_unfair_lock 是一个低等级锁。一些高等级的锁才应该是我们日常开发中的首选。
  2. 必须使用加锁时的同一个线程来进行解锁,尝试使用不同的线程来解锁将导致断言中止进程。( os_unfair_lock_assert_owner 函数判断当前线程是否是 os_unfair_lock 的所有者,否则触发断言。)
  3. 锁里面包含线程所有权信息来解决优先级反转问题。
  4. 不能通过共享或多重映射内存从多个进程或线程访问此锁,锁的实现依赖于锁值的地址和所属进程。
  5. 必须使用 OS_UNFAIR_LOCK_INIT 进行初始化。

 pthread_mutex_t 是 C 语言下多线程互斥锁的方式,是跨平台使用的锁,等待锁的线程会处于休眠状态,可根据不同的属性配置把 pthread_mutex_t 初始化为不同类型的锁,例如:互斥锁、递归锁、条件锁。当使用递归锁时,允许同一个线程重复进行加锁,另一个线程访问时就会等待,这样可以保证多线程时访问共用资源的安全性。

PTHREAD_MUTEX_NORMAL // 缺省类型,也就是普通类型,当一个线程加锁后,其余请求锁的线程将形成一个队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK // 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁
PTHREAD_MUTEX_RECURSIVE // 递归锁,允许同一个线程对同一锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT // 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。

pthread_mutex_trylocktrylock 不同,trylock 返回的是 YESNOpthread_mutex_trylock 加锁成功返回的是 0,失败返回的是错误提示码。

 NSLock 继承自 NSObject 并遵循 NSLocking 协议,lock 方法加锁,unlock 方法解锁,tryLock 尝试并加锁,如果返回 true 表示加锁成功,返回 false 表示加锁失败,谨记返回的 BOOL 表示加锁动作的成功或失败,并不是能不能加锁,即使加锁失败也会不会阻塞当前线程。lockBeforeDate: 是在指定的 Date 之前尝试加锁,如果在指定的时间之前都不能加锁,则返回 NO,且会阻塞当前线程。大概可以使用在:先预估上一个临界区的代码执行完毕需要多少时间,然后在这个时间之后为另一个代码段来加锁。

  1. 基于 mutex 基本锁的封装,更加面向对象,等待锁的线程会处于休眠状态。
  2. 遵守 NSLocking 协议,NSLocking 协议中仅有两个方法 -(void)lock-(void)unlock
  3. 可能会用到的方法:
  4. 初始化跟其他 OC 对象一样,直接进行 allocinit 操作。
  5. -(void)lock; 加锁。
  6. -(void)unlock; 解锁。
  7. -(BOOL)tryLock; 尝试加锁。
  8. -(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁。
  9. 在当前线程连续调用 [self.lock lock] 加锁会导致当前线程死锁。
  10. 在主线程没有获取 Lock 的情况下和在获取 Lock 的情况下,连续两次 [self.lock unlock] 都不会发生异常。(其他的锁可能连续解锁的情况下会导致 crash,还没有来的及测试)
  11. 同时子线程死锁会导致 ViewController 不释放。

 一个线程在加锁的时候,其余请求锁的的线程将形成一个等待队列,按先进先出原则,这个结果可以通过修改线程优先级进行测试得出。

 NSRecursiveLock 是递归锁,和 NSLock 的区别在于,它可以在同一个线程中重复加锁也不会导致死锁。NSRecursiveLock 会记录加锁和解锁的次数,当二者次数相等时,此线程才会释放锁,其它线程才可以加锁成功。

  1. NSLock 一样,也是基于 mutex 的封装,不过是基于 mutex 递归锁的封装,所以这是一个递归锁。
  2. 遵守 NSLocking 协议,NSLocking 协议中仅有两个方法 -(void)lock-(void)unlock
  3. 可能会用到的方法:
  4. 继承自 NSObject,所以初始化跟其他 OC 对象一样,直接进行 alloc 和 init 操作。
  5. -(void)lock; 加锁
  6. -(void)unlock; 解锁
  7. -(BOOL)tryLock; 尝试加锁
  8. -(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁。
  9. 递归锁是可以在同一线程连续调用 lock 不会直接导致阻塞死锁,但是依然要执行相等次数的 unlock。不然异步线程再获取该递归锁会导致该异步线程阻塞死锁。
  10. 递归锁允许同一线程多次加锁,不同线程进入加锁入口会处于等待状态,需要等待上一个线程解锁完成才能进入加锁状态。

 NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试,NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。也就是使用 NSCondition 的模型为: 1. 锁定条件对象。2. 测试是否可以安全的履行接下来的任务。如果布尔值为假,调用条件对象的 wait 或者 waitUntilDate: 方法来阻塞线程。再从这些方法返回,则转到步骤 2 重新测试你的布尔值。(继续等待信号和重新测试,直到可以安全的履行接下来的任务,waitUntilDate: 方法有个等待时间限制,指定的时间到了,则返回 NO,继续运行接下来的任务。而等待信号,既线程执行 [lock signal] 发送的信号。其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程,如果没有等待的线程,这两个方法都没有作用。)

  1. 基于 mutex 基础锁和 cont 条件的封装,所以它是互斥锁且自带条件,等待锁的线程休眠。
  2. 遵守 NSLocking 协议,NSLocking 协议中仅有两个方法 -(void)lock-(void)unlock
  3. 可能会用到的方法
  4. 初始化跟其它 OC 对象一样,直接进行 allocinit 操作。
  5. -(void)lock; 加锁
  6. -(void)unlock; 解锁
  7. -(BOOL)tryLock; 尝试加锁
  8. -(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁
  9. -(void)wait; 等待条件(进入休眠的同时放开锁,被唤醒的同时再次加锁)
  10. -(void)signal; 发送信号激活等待该条件的线程,切记线程收到后是从 wait 状态开始的
  11. - (void)broadcast; 发送广播信号激活等待该条件的所有线程,切记线程收到后是从 wait 状态开始的

 NSConditionLock 和 NSLock 类似,同样是继承自 NSObject 和遵循 NSLocking 协议,加解锁 try 等方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如 tryLock 和 tryLockWhenCondition:,NSConditionLock 可以称为条件锁。只有 condition 参数与初始化的时候相等或者上次解锁后设置的 condition 相等,lock 才能正确的进行加锁操作。unlockWithCondition: 并不是当 condition 符合条件时才解锁,而是解锁之后,修改 condition 的值为入参,当使用 unlock 解锁时, condition 的值保持不变。如果初始化用 init,则 condition 默认值为 0。lockWhenCondition: 和 lock 方法类似,加锁失败会阻塞当前线程,一直等下去,直到能加锁成功。tryLockWhenCondition: 和 tryLock 类似,表示尝试加锁,即使加锁失败也不会阻塞当前线程,但是同时满足 lock 是空闲状态并且 condition 符合条件才能尝试加锁成功。从上面看出,NSConditionLock 还可以实现任务之间的依赖。

 @synchronized(object) 指令使用的 object 为该锁的唯一标识,只有当标识相同时,才满足互斥,所以如果线程 2 中的 @synchronized(self) 改为 @synchronized(self.view),则线程 2 就不会被阻塞,@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动释放互斥锁。@synchronized 还有一个好处就是不用担心忘记解锁了。如果在 @synchronized(object) {} 内部 object 被释放或被设为 nil,从测试结果来看,不会产生问题,但如果 object 一开始就是 nil,则失去了加锁的功能。不过虽然 nil 不行,但是 [NSNull null] 是可以的。

objc4-750 版本之前(iOS 12 之前)@synchronized 是一个基于 pthread_mutex_t 封装的递归锁,之后实现则发生了改变,底层的封装变为了 os_unfair_lock

 dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号量,一个是发送信号。 dispatch_semaphore 和 NSCondition 类似,都是一种基于信号的同步方式,但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。

 dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始化值为 1。注意,这里的传入参数必须大于等于 0,否则 dispatch_semaphore 会返回 NULL。

 dispatch_semaphore_wait(signal, overTime) 方法会判断 signal 的信号值是否大于 0,大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。

 dispatch_semaphore_signal(signal) 发送信号,如果没有等待的线程调用信号,则使 signal 信号值加 1(做到对信号的保存)。一个 dispatch_semaphore_wait(signal, overTime) 方法会去对应一个 dispatch_semaphore_signal(signal) 看起来像 NSLock 的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatcch_semaphore 的信号量初始值为 x,则可以有 x 个线程同时访问被保护的临界区。

  1. 本来是用于控制线程的最大并发数量,我们将并发数量设置为 1 也可以认为是加锁的功能。
  2. 可能会用到的方法:
  3. 初始化 dispatch_semaphore_create() 传入的值为最大并发数量,设置为 1 则达到加锁效果。
  4. 判断信号量的值 dispatch_semaphore_wait() 如果大于 0,则可以继续往下执行(同时信号量的值减去 1),如果信号量的值为 0,则线程进入休眠状态等待(此方法的第二个参数就是设置要等多久,一般是使用永久 DISPATCH_TIME_FOREVER)。
  5. 释放信号量 dispatch_semaphore_signal() 同时使信号量的值加上 1

 pthread_rwlock_t 读写锁,首先引入一个问题:“如何实现一个多读单写的模型?”,需求如下:

  • 同时可以有多个线程读取。
  • 同时只能有一个线程写入。
  • 同时只能执行读取或者写入的一种。

 首先想到的就是我们的 pthread_rwlock_t

  1. 读取加锁可以同时多个线程进行,写入同时只能一个线程进行,等待的线程处于休眠状态。

  2. 可能会用到的方法:

  3. pthread_rwlock_init() 初始化一个读写锁

  4. pthread_rwlock_rdlock() 读写锁的读取加锁

  5. pthread_rwlock_wrlock() 读写锁的写入加锁

  6. pthread_rwlock_unlock() 解锁

  7. pthread_rwlock_destroy() 销毁锁

 dispatch_barrier_async 实现多读单写。

  1. 传入的并发队列必须是 dispatch_queue_create() 方式手动创建的,如果传入串行队列或者通过 dispatch_get_global_queue() 方式获取一个全局并发队列,则 dispatch_barrier_async 的作用就跟 dispatch_async 变的一样了。
  2. 可能会用到的方法:
  3. dispatch_queue_create() 创建并发队列
  4. dispatch_barrier_async() 异步栅栏

 锁粗略的效率排序(不同的锁可能更擅长不同的场景)

  1. os_unfair_lock (iOS 10 之后)
  2. OSSpinLock (iOS 10 之前)
  3. dispatch_semaphore (iOS 版本兼容性好)
  4. pthread_mutex_t (iOS 版本兼容性好)
  5. NSLock (基于 pthread_mutex_t 封装)
  6. NSCondition (基于 pthread_mutex_t 封装)
  7. pthread_mutex_t(recursive) 是递归锁的优先推荐
  8. NSRecursiveLock (基于 pthread_mutex_t 封装)
  9. NSConditionLock (基于 NSCondition 封装)
  10. @synchronized
  11. iOS 12 之前基于 pthread_mutex_t 封装
  12. iOS 12 之后基于 os_unfair_lock 封装(iOS 12 之后它的效率应该不是最低,应该排行在 3/4 左右)

 自旋锁和互斥锁怎么取舍选择,其实这个问题已经没有什么意义,因为自旋锁 OSSpinLock 在 iOS 10 之后已经废弃了,而它的替换方案 os_unfair_lock 是互斥锁,但是我们仍然做一下对比: 自旋锁:

  • 预计线程需要等待的时间较短
  • 多核处理器
  • CPU 的资源不紧张

互斥锁:

  • 预计线程需要等待的时间较长
  • 单核处理器
  • 临界区(加锁解锁之间的部分)有 I/O 操作(即需要等待的时间较长时)

13. Pthreads 和 NSThread 介绍。

 可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是 IEEE(电气和电子工程师协会)为要在各种 UNIX 操作系统上运行软件,而定义 API 的一系列互相关联的标准的总称。

 Pthreads 一般指 POSIX 线程。 POSIX 线程(POSIX Threads,常被缩写为 Pthreads)是 POSIX 的线程标准,定义了创建和操纵线程的一套 API。

pthread_create 是类 Unix 操作系统(Unix、Linux、Mac OS X等)的创建线程的函数。它的功能是创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数。 pthread_create 的返回值: 若成功,返回 0;若出错,返回出错编号,并且 pthread_t * __restrict 中的内容未定义。

 线程的合并是一种主动回收线程资源的方案。当一个进程或线程调用了针对其它线程的 pthread_join 函数,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。当被合并线程结束,pthread_join 接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是 pthread_detach

 线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得 pthread_detach 接口只要拥有一个参数就行了,那就是被分离线程句柄。

线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。

 pthread_setcanceltype 设置本线程取消动作的执行时机,设置本线程取消动作的执行时机,type 有两种取值:PTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS,仅当 Cancel 状态为 Enable 时有效,分别表示收到信号后继续运行至下一个取消点再退出 (推荐做法,因为在终止线程之前必须要处理好内存回收防止内存泄漏,而手动设置取消点这种方式就可以让我们很自由的处理内存回收时机)和立即执行取消动作(退出)(不推荐这样操作,可能造成内存泄漏等问题);oldtype 如果不为 NULL 则存入原来的取消动作类型值。

void pthread_testcancel(void); 检查本线程是否处于 Canceld 状态,如果是,则进行取消动作,否则直接返回。 此函数在线程内执行,执行的位置就是线程退出的位置,在执行此函数以前,线程内部的相关资源申请一定要释放掉,否则很容易造成内存泄露。线程取消的方法是向目标线程发 Cancel 信号,但如何处理 Cancel 信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至 Cancelation-point(取消点),由不同的 Cancelation 状态决定。线程接收到 CANCEL 信号的缺省处理(即 pthread_create 创建线程的缺省状态)是继续运行至取消点,也就是说设置一个 CANCELED 状态,线程继续运行,只有运行至 Cancelation-point 的时候才会退出。

 线程通过调用 pthread_exit 函数终止执行,就如同进程在结束时调用 exit 函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。

 同一进程内线程之间可以共享内存地址空间,线程之间的数据交换可以非常快捷,这是线程最显著的优点。但是多个线程访问共享数据,需要昂贵的同步开销(加锁),也容易造成与同步相关的 BUG,更麻烦的是有些数据根本就不希望被共享,这又是缺点。

 从现代技术角度看,在很多时候使用多线程的目的并不是为了对共享数据进行并行处理。更多是由于多核心 CPU 技术的引入,为了充分利用 CPU 资源而进行并行运算(不互相干扰)。换句话说,大多数情况下每个线程只会关心自己的数据而不需要与别人同步。

 一个 NSThread 对象会对应一个线程,与 Pthreads 相比,它以更加面向对象的方式来操作和管理线程,尽管还是需要我们自己手动管理线程的生命周期,但是此时仅限于创建,我们这里可以把创建线程的过程理解为创建 NSThread 对象,至于最后任务执行结束,线程资源的回收,系统都会帮我们处理,所以相比 GCD 来说还不是最易用的,GCD 的使用过程中,我们可以完全不考虑线程的创建和销毁。

 使用 NSThread 的类方法显式的创建线程并会立刻自动启动线程(对比上面不需要再调用 start 函数)。


14. GCD 简单介绍。

 Grand Central Dispatch (GCD) 是 Apple 开发的一个多核编程的较新的解决方法。通过提交工作到 dispatch 系统管理的队列(dispatch queues),在多核硬件上同时执行代码。主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。可以理解为 Dispatch 队列封装了底层多核系统调度的操作,我们只需要关心对 Dispatch 队列的操作,不需要关心任务到底分配给哪个核心,甚至不需要关心任务在哪个线程执行(当然为了深入学习主线程和子线程是一定要研究的)。

@protocol OS_dispatch_queue <OS_dispatch_object>
@end

typedef NSObject<OS_dispatch_queue> * dispatch_queue_t;

OS_dispatch_queue 是继承自 OS_dispatch_object 协议的协议,并且为遵循该协议的 NSObject 实例对象类型的指针定义了一个 dispatch_queue_t 的别名,看到这里我们恍然大悟,例如我们整天使用的 dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 获取一个全局并发对象,而这个 globalQueue 其实就是一个遵循 OS_dispatch_queue 协议的 NSObject 实例对象指针(OC 下:dispatch_queue_t 是 NSObject 指针)。

 Dispatch 是用于通过简单但功能强大的 API 来表达并发性的抽象模型。在核心上,dispatch 提供了可以向其提交 blocks 的串行 FIFO 队列。提交给这些 dispatch queues 的 blocks 在系统完全管理的线程池上调用,无法保证将在哪个线程上调用 block(系统会自行从线程池取可用的线程);但是,它保证一次只调用一个提交到 FIFO dispatch queue 的 block。当多个队列有要处理的块时,系统可以自由地分配额外的线程来并发地调用这些 blocks。当队列变为空时,这些线程将自动释放。

 Dispatch queues 有多种形式,最常见的一种是调度串行队列(dispatch_queue_serial_t)。系统管理一个线程池,该线程池处理 dispatch queues 并调用提交给它们的工作项。从概念上讲,一个 dispatch queue 可以具有自己的执行线程,并且队列之间的交互是高度异步的。调度队列通过调用 dispatch_retain() 和 dispatch_release() 进行引用计数操作。提交给队列的待处理工作项也会保留对该队列的引用,直到它们完成为止。一旦释放了对队列的所有引用,系统将重新分配该队列(queue 被释放销毁)。

 调度全局并发队列(dispatch global concurrent queues)在系统管理的线程池之上提供优先级桶,系统将根据需求和系统负载决定分配给这个池的线程数。特别是,系统会尝试为该资源保持良好的并发级别,并且当系统调用中有太多的现有工作线程阻塞时,将创建新线程。(NSThread 和 GCD 的一个重大区别,GCD 下线程都是系统自动创建分配的,而 NSThread 则是自己手动创建线程或者自己手动开启线程。)

 全局并发队列(global concurrent queues)是共享资源,因此,此资源的每个用户都有责任不向该池提交无限数量的工作,尤其是可能阻塞的工作,因为这可能导致系统产生大量线程(又名:线程爆炸 thread explosion)。

 提交到全局并发队列(global concurrent queues)的工作项相对于提交顺序没有排序保证,并且提交到这些队列的工作项可以并发调用(毕竟本质还是并发队列)。

typedef void (^dispatch_block_t)(void); 日常使用 GCD 向队列提交的工作项都是这种名字是 dispatch_block_t 参数和返回值都是 void 的 Block。

 提交给调度队列(dispatch queues)的 blocks 的类型,不带任何参数且没有返回值。当不使用 Objective-C ARC 进行构建时,分配到堆上或复制到堆上的 block 对象必须通过 -[release] 消息或 Block_release() 函数释放。

 以字面量形式声明的 block 分配空间存储在栈上。必须使用 Block_copy() 函数或通过发送 -[copy] 消息将 block 复制到堆中。

dispatch_async 提交一个 block 以在调度队列上异步执行。对 dispatch_async 函数的调用总是在 block 被提交后立即返回,而从不等待 block 被调用。(即我们熟悉的异步调用不会阻塞当前线程,因为 dispatch_async 函数提交 block 以后就立即返回了,对应的 dispatch_sync 函数调用则是等 block 执行结束以后才会返回。我们潜意识里可能觉的 dispatch_async 函数所提交的队列类型不同会影响该函数是否立即返回 ,这里 dispatch_async 函数是否立即返回和提交的队列类型是完全无关的,dispatch_async 函数不管是提交 block 到并发队列还是串行队列都会立即返回,不会阻塞当前线程。)

dispatch_async 函数中,目标队列(dispatch_queue_t queue)决定是串行调用队列中的块还是同时并发调用提交到该队列中的块。(当 queue 是并发队列时会开启多条线程并发执行所有的 block,如果 queue 是串行队列(除了主队列)的话则是仅开辟一条线程串行执行所有的 block,如果主队列的话则是不开启线程直接在主线程中串行执行所有的 block),dispatch_async 函数提交 block 到不同的串行队列,则这些串行队列是相互并行处理的。(它们在不同的线程中并发执行串行队列中的 block。)

dispatch_sync 提交一个 block 以在调度队列上同步执行。用于将工作项提交到一个 dispatch queue,像 dispatch_async 函数一样,但是 dispatch_sync 在工作项完成之前不会返回。(即 dispatch_sync 函数只有在提交到队列的 block 执行完成以后才会返回,会阻塞当前线程)。

dispatch_apply 将一个 block 提交给调度队列以进行并行调用。(快速迭代)

 Dispatch barrier API 是一种机制,用于将屏障块(barrier blocks)提交给调度队列,类似于 dispatch_async/dispatch_sync API。它可以实现有效的读取器/写入器方案。

 当提交到 全局队列 或未使用 DISPATCH_QUEUE_CONCURRENT 属性创建的队列时,屏障块的行为与使用 dispatch_async/dispatch_sync API 提交的块相同。(如果使用 dispatch_asyncdispatch_barrier_async 提交到 dispatch_get_global_queue 取得的 queue,则并发执行,失去屏障的功能。)

queue:块提交到的目标调度队列。系统将在目标队列上保留引用,直到该块完成为止。在此参数中传递 NULL 的结果是不确定的。

block:提交到目标调度队列的块。该函数代表调用者执行 Block_copyBlock_release。在此参数中传递 NULL 的结果是不确定的。

dispatch_barrier_sync 提交屏障块(barrier block)以在调度队列上同步执行(会阻塞当前线程,直到 barrier block 执行完成才会返回)。


15. dispatch_object_t

 默认情况下,在使用 Objective-C 编译器进行构建时,libSystem 对象(例如 GCD 和 XPC 对象)被声明为 Objective-C 类型,这使他们可以参与 ARC,通过 Blocks 运行时参与 RR 管理以及通过静态分析器参与泄漏检查,并将它们添加到 Cocoa 集合中。

dispatch_group_wait 同步等待,直到与一个组关联的所有块都已完成,或者直到指定的超时时间过去为止。

 该函数等待与给定调度组关联的块的完成,并在所有块完成或指定的超时时间结束后返回。(阻塞直到函数返回)

 如果没有与调度组关联的块(即该组为空),则此函数将立即返回。

 从多个线程同时使用同一调度组调用此函数的结果是不确定的。

 成功返回此函数后,调度组为空。可以使用 dispatch_release 释放它,也可以将其重新用于其他块。

dispatch_group_notify 当与组相关联的所有块都已完成时,计划将块提交到队列(即当与组相关联的所有块都已完成时,提交到 queueblock 将执行)。

 如果没有块与调度组相关联(即该组为空),则通知块将立即提交。


16. dispatch_source_t/dispatch_block_t

 dispatch framework 提供了一套接口,用于监视低级系统对象(file descriptors(文件描述符), Mach ports, signals, VFS nodes, etc.)的活动,并在此类活动发生时自动向 dispatch queues 提交事件处理程序块(event handler blocks)。这套接口称为 Dispatch Source API。

 DISPATCH_SOURCE_TYPE_TIMER 基于计时器(based on a timer)提交事件处理程序块(event handler block)的调度源(dispatch source)。句柄(handle)未使用(现在传递零)。mask 指定要应用的来自 dispatch_source_timer_flags_t 的标志。

#define DISPATCH_SOURCE_TYPE_TIMER (&_dispatch_source_type_timer)
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_SOURCE_TYPE_DECL(timer);

void dispatch_source_cancel(dispatch_source_t source); 取消操作将阻止对指定调度源的事件处理程序块(event handler block)的任何进一步调用,但不会中断已在进行中的事件处理程序块。

 一旦源的事件处理程序完成,取消处理程序将提交到源的目标队列,这表明现在可以安全地关闭源的句柄(i.e. file descriptor or mach port)。

__builtin_expect 这个指令是 GCC 引入的,作用是允许程序员将最有可能执行的分支告诉编译器,这个指令的写法为:__builtin_expect(EXP, N),意思是:EXP == N 的概率很大,然后 CPU 会预取该分支的指令,这样 CPU 流水线就会很大概率减少了 CPU 等待取指令的耗时,从而提高 CPU 的效率。

dispatch_block_t dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block); 根据现有块(existing block)和给定的标志(flags)在堆(heap)上创建一个新的调度块对象(dispatch block object)。提供的块被 Block_copy 到堆中,并由新创建的调度块对象保留。

 返回的调度块对象(dispatch block object)旨在通过 dispatch_async 和相关函数提交给调度队列,但也可以直接调用。两种操作都可以执行任意次,但只有第一次完成的调度块对象(dispatch block object)的执行才能用 dispatch_block_wait 等待,或用 dispatch_block_notify 来观察。

long dispatch_block_wait(dispatch_block_t block, dispatch_time_t timeout); 同步等待(阻塞),直到指定的分发块对象的执行完成或指定的超时时间结束为止。如果块对象的执行已经完成,该函数将立即返回。

 不能使用此接口等待同一块对象的多次执行;为此,请使用 dispatch_group_wait。单个调度块对象(dispatch block object)可以等待一次并执行一次,也可以执行任意次数。任何其他组合的行为都是未定义的。即使取消(dispatch_block_cancel)表示该块的代码从不运行,向调度队列的提交也被视为执行。

void dispatch_block_notify(dispatch_block_t block, dispatch_queue_t queue, dispatch_block_t notification_block); 计划在完成指定调度块对象(block)的执行后将通知块(notification_block)提交给队列。

 如果观察到的块对象(block)的执行已经完成,则此函数将立即提交通知块(notification_block)。

 使用此接口无法通知同一块对象的多次执行,请为此目的使用 dispatch_group_notify

void dispatch_block_cancel(dispatch_block_t block); 异步取消指定的调度块对象(dispatch block object)。

 取消会使调度块对象(dispatch block object)的任何将来执行立即返回,但不影响已经在进行中的块对象的任何执行。

 与该块对象相关联的任何资源的释放将被延迟,直到下一次尝试执行该块对象(或已完成的任何执行完成)为止。

 注意:需要注意确保可以取消的块对象不会捕获需要执行块体才能释放的任何资源(例如,块体调用 free(用 malloc 分配的内存))。如果由于取消而从不执行块体,则这些资源将被泄漏。(如我们熟悉的只有 block 执行才能破开循环引用环的操作)


17. GCD 获取主队列/获取一个全局队列/创建自定义队列的过程。

#define DISPATCH_QUEUE_SERIAL NULL 用于创建以 FIFO 顺序串行调用块的调度队列(串行队列)的属性,值是 NULL

_dispatch_lane_create_with_target 函数进行队列创建,主体分为两个大步,首先规范化参数,然后是调用 dispatch_lane_t dq = _dispatch_object_alloc(vtable, sizeof(struct dispatch_lane_s)); 申请一个结构体变量后进行初始化操作。

DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa, dispatch_queue_t tq, bool legacy) {

    // 1. 如果 dqa 是 DISPATCH_QUEUE_SERIAL(值是 NULL)作为入参传入的话,会直接返回一个空的 dispatch_queue_attr_info_t 结构体实例,(dispatch_queue_attr_info_t dqai = { };)。
    
    // 2. 如果 dqa 是 DISPATCH_QUEUE_CONCURRENT(值是全局变量 _dispatch_queue_attr_concurrent)作为入参传入的话,会返回一个 dqai_concurrent 值是 true 的 dispatch_queue_attr_info_t 结构体实例,(dqai_concurrent 为 true 表示是并发队列)。
    
    // 3. 第三种情况则是传入自定义的 dispatch_queue_attr_t 时,
    //    则会进行取模和取商运算为 dispatch_queue_attr_info_t 结构体实例的每个成员变量赋值后返回该 dispatch_queue_attr_info_t 结构体实例。
    
    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);

    
    // Step 1: Normalize arguments (qos, overcommit, tq) 规范化参数
    
    dispatch_qos_t qos = dqai.dqai_qos; //(dqai_qos 表示线程优先级)
    
    // 如果 HAVE_PTHREAD_WORKQUEUE_QOS 为假会进行一个 dqai_qos 的切换
#if !HAVE_PTHREAD_WORKQUEUE_QOS
    if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
        // 如果是 "用户交互" 这个最高优先级,则切到 "用户启动" 这个第二优先级
        dqai.dqai_qos = qos = DISPATCH_QOS_USER_INITIATED;
    }
    
    if (qos == DISPATCH_QOS_MAINTENANCE) {
        // 如果是 "QOS_CLASS_MAINTENANCE" 这个最低优先级,则切到 "后台线程" 这个倒数第二优先级
        dqai.dqai_qos = qos = DISPATCH_QOS_BACKGROUND;
    }
#endif // !HAVE_PTHREAD_WORKQUEUE_QOS

    // 取出是否允许 "过量使用(超过物理上的核心数)"
    _dispatch_queue_attr_overcommit_t overcommit = dqai.dqai_overcommit;
    
    if (overcommit != _dispatch_queue_attr_overcommit_unspecified && tq) {
        // 如果 overcommit 不等于 "未指定 overcommit" 并且 tq 不为空
        //(已知上面 dispatch_queue_create 函数调用默认入参 DISPATCH_TARGET_QUEUE_DEFAULT 是 NULL)
        if (tq->do_targetq) {
            // crash
            DISPATCH_CLIENT_CRASH(tq, "Cannot specify both overcommit and "
                    "a non-global target queue");
        }
    }

    if (tq && dx_type(tq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE) {
        // Handle discrepancies between attr and target queue, attributes win
        // 处理 attr 和目标队列之间的差异,以 attr 为主
        
        // 如果目标队列存在,且目标队列是全局根队列
        if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
            // 如果 overcommit 是未指定
            if (tq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) {
                // 如果目标队列的优先级是 DISPATCH_PRIORITY_FLAG_OVERCOMMIT,则把 overcommit 置为允许
                overcommit = _dispatch_queue_attr_overcommit_enabled;
            } else {
                // 否则是不允许
                overcommit = _dispatch_queue_attr_overcommit_disabled;
            }
        }
        
        // 如果优先级未指定,则新创建的队列的优先级继承目标队列的优先级
        if (qos == DISPATCH_QOS_UNSPECIFIED) {
            qos = _dispatch_priority_qos(tq->dq_priority);
        }
        
        // tq 置 NULL
        tq = NULL;
    } else if (tq && !tq->do_targetq) {
        // target is a pthread or runloop root queue, setting QoS or overcommit is disallowed
        // target queue 是一个 pthread 或 runloop root queue, 设置 QoS 或 overcommit 是不允许的
        
        if (overcommit != _dispatch_queue_attr_overcommit_unspecified) {
            // 如果 tq 存在且 overcommit 不是未指定的话,则 crash
            DISPATCH_CLIENT_CRASH(tq, "Cannot specify an overcommit attribute " "and use this kind of target queue");
        }
    } else {
        // tq 为 NULL 的情况
        
        if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
            // Serial queues default to overcommit! (串行队列默认为 overcommit)
            // 根据上面的入参知道,串行队列的 dqai_concurrent 为 false,并发队列的 dqai_concurrent 为 true。
            
            // 当 dqai.dqai_concurrent 为 true,不允许 overcommit,否则允许 overcommit
            overcommit = dqai.dqai_concurrent ?
                    _dispatch_queue_attr_overcommit_disabled :
                    _dispatch_queue_attr_overcommit_enabled;
        }
    }
    
    // 当 tq 为 NULL,即入参目标队列为 DISPATCH_TARGET_QUEUE_DEFAULT(值是 NULL) 时,
    // 根据 qos 和 overcommit 从 _dispatch_root_queues 这个全局的根队列数组中获取一个根队列作为新队列的目标队列,
    // (return &_dispatch_root_queues[2 * (qos - 1) + overcommit]; 根据这两个参数计算下标后直接从数组中读取)
    // (👇👇 _dispatch_root_queues 全局的根队列数组 如下段代码)
    
    if (!tq) {
        tq = _dispatch_get_root_queue(
                qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
                overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq;
                
        if (unlikely(!tq)) {
            // 如果未取得目标队列则 crash
            DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
        }
    }

    //
    // Step 2: Initialize the queue(初始化队列)
    //
    
    // dispatch_queue_create 函数的调用中,legacy 默认传的是 true
    if (legacy) {
        // if any of these attributes is specified, use non legacy classes
        // 如果指定了这些属性中的任何一个,请使用非旧类
        
        // 活动状态(dqai_inactive)和自动释放频率(dqai_autorelease_frequency)
        if (dqai.dqai_inactive || dqai.dqai_autorelease_frequency) {
            legacy = false;
        }
    }

    const void *vtable;
    dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
    if (dqai.dqai_concurrent) {
        // 并发队列
        vtable = DISPATCH_VTABLE(queue_concurrent); // _dispatch_queue_concurrent_vtable 包裹队列可进行的函数调用
    } else {
        // 串行队列
        vtable = DISPATCH_VTABLE(queue_serial); // _dispatch_queue_serial_vtable 包裹队列可进行的函数调用
    }
    
    // 自动释放频率
    switch (dqai.dqai_autorelease_frequency) {
    case DISPATCH_AUTORELEASE_FREQUENCY_NEVER:
        dqf |= DQF_AUTORELEASE_NEVER;
        break;
    case DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:
        dqf |= DQF_AUTORELEASE_ALWAYS;
        break;
    }
    
    // 队列标签
    if (label) {
        // _dispatch_strdup_if_mutable 函数的功能:如果 label 入参是可变的字符串则申请空间并复制原始字符串进入,如果 label 入参是不可变字符串则直接返回原始值
        const char *tmp = _dispatch_strdup_if_mutable(label);
        
        if (tmp != label) {
            // 新申请了空间
            dqf |= DQF_LABEL_NEEDS_FREE;
            // "新值" 赋给 label
            label = tmp;
        }
    }

    // void *_dispatch_object_alloc(const void *vtable, size_t size) 函数未找到其定义,只在 object_internal.h 中看到其声明。
    // dispatch_lane_s 是 dispatch_queue_s 的子类。
    
    // dq 是一个指向 dispatch_lane_s 结构体的指针
    dispatch_lane_t dq = _dispatch_object_alloc(vtable,
            sizeof(struct dispatch_lane_s));
            
    // 当 dqai.dqai_concurrent 为真时入参为 DISPATCH_QUEUE_WIDTH_MAX(4094)否则是 1
    // 当 dqai.dqai_inactive 为真时表示非活动状态,否则为活动状态
    // #define DISPATCH_QUEUE_ROLE_INNER            0x0000000000000000ull
    // #define DISPATCH_QUEUE_INACTIVE              0x0180000000000000ull
    
    // 初始化 dq
    _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
            (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

    // 队列签名
    dq->dq_label = label;
    
    // 优先级
    dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos, dqai.dqai_relpri);
    // overcommit
    if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
        dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
    }
    
    // 如果是非活动状态
    if (!dqai.dqai_inactive) {
        // 新队列的优先级继承自目标队列优先级
        _dispatch_queue_priority_inherit_from_target(dq, tq);
        _dispatch_lane_inherit_wlh_from_target(dq, tq);
    }
    
    // 目标队列的内部引用计数加 1(原子操作)
    _dispatch_retain(tq);
    
    // 设置新队列的目标队列
    dq->do_targetq = tq;
    
    // DEBUG 时的打印函数
    _dispatch_object_debug(dq, "%s", __func__);
    return _dispatch_trace_queue_create(dq)._dq;
}

_dispatch_root_queues 是一个长度为 12 的队列数组。

// 6618342 Contact the team that owns the Instrument DTrace probe before renaming this symbol
struct dispatch_queue_global_s _dispatch_root_queues[] = {
    _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, 0,
        .dq_label = "com.apple.root.maintenance-qos",
        .dq_serialnum = 4,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.maintenance-qos.overcommit",
        .dq_serialnum = 5,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
        .dq_label = "com.apple.root.background-qos",
        .dq_serialnum = 6,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.background-qos.overcommit",
        .dq_serialnum = 7,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
        .dq_label = "com.apple.root.utility-qos",
        .dq_serialnum = 8,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.utility-qos.overcommit",
        .dq_serialnum = 9,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
        .dq_label = "com.apple.root.default-qos",
        .dq_serialnum = 10,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
            DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.default-qos.overcommit",
        .dq_serialnum = 11,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
        .dq_label = "com.apple.root.user-initiated-qos",
        .dq_serialnum = 12,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.user-initiated-qos.overcommit",
        .dq_serialnum = 13,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
        .dq_label = "com.apple.root.user-interactive-qos",
        .dq_serialnum = 14,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.user-interactive-qos.overcommit",
        .dq_serialnum = 15,
    ),
};

 主队列的话是一个全局变量 _dispatch_main_q,我们日常获取主队列,就是直接获取它。

struct dispatch_queue_static_s _dispatch_main_q = {
    DISPATCH_GLOBAL_OBJECT_HEADER(queue_main), // 继承自父类
    
#if !DISPATCH_USE_RESOLVERS

    // 是否有目标队列(从根队列数组中取出 DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS + !!(overcommit) 为下标的队列作为目标队列)
    // #define _dispatch_get_default_queue(overcommit) \
    //         _dispatch_root_queues[DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS + \
    //                 !!(overcommit)]._as_dq
    
    .do_targetq = _dispatch_get_default_queue(true),
#endif
    
    .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) | DISPATCH_QUEUE_ROLE_BASE_ANON, // (0xfffull << 41) | 0x0000001000000000ull
    .dq_label = "com.apple.main-thread", // 队列标签(队列名)
    .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1), // 并发数是 1,即为串行队列
    .dq_serialnum = 1, // 队列序号是 1
};

 我们日常用的获取主队列的方法 dispatch_get_main_queue,实现即为获取 _dispatch_main_q 变量。

// 强制类型转换
#define DISPATCH_GLOBAL_OBJECT(type, object) ((OS_OBJECT_BRIDGE type)&(object))

DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_CONST DISPATCH_NOTHROW
dispatch_queue_main_t dispatch_get_main_queue(void) {

    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
    
}

dispatch_get_global_queue 也是根据入参直接从 _dispatch_root_queues 这个全局的根队列数组中获取一个队列。

dispatch_queue_global_t dispatch_get_global_queue(long priority, unsigned long flags) {
    dispatch_assert(countof(_dispatch_root_queues) ==
            DISPATCH_ROOT_QUEUE_COUNT);

    if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
        return DISPATCH_BAD_INPUT;
    }
    dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority);
    
#if !HAVE_PTHREAD_WORKQUEUE_QOS
    if (qos == QOS_CLASS_MAINTENANCE) {
        qos = DISPATCH_QOS_BACKGROUND;
    } else if (qos == QOS_CLASS_USER_INTERACTIVE) {
        qos = DISPATCH_QOS_USER_INITIATED;
    }
#endif

    if (qos == DISPATCH_QOS_UNSPECIFIED) {
        return DISPATCH_BAD_INPUT;
    }
    
    // 直接从 _dispatch_root_queues 中读取。
    return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT);
}

18. dispatch_async 函数执行流程分析。

 当我们向队列提交任务时,不管是用 dispatch_async 向队列中异步提交 block,还是用 dispatch_async_f 向队列中异步提交函数,都会把提交的任务包装成 dispatch_continuation_s,而在 dispatch_continuation_s 结构体中是使用一个函数指针(dc_func)来存储要执行的任务的,当提交的是 block 任务时 dispatch_continuation_s 内部存储的是 block 结构体定义的函数,而不是 block 本身。

dispatch_async 函数的实现代码在逻辑上可以分为两个部分,第一部分对 work 函数封装(_dispatch_continuation_init)(内部会把 block 复制到堆区 _dispatch_Block_copy(work),即我们使用 dispatch_async 函数时创建的栈区 block 会被直接复制到堆区去),第二部分是对线程的调用(_dispatch_continuation_async)并发处理提交的任务函数。

 首先我们做出一个总结:

  • dispatch_async 函数提交到主队列(dispatch_get_main_queue())中的多个 block 任务只在主线程中串行执行,不会开启新线程。

  • dispatch_async 函数提交到同一自定义串行队列中的多个 block 任务会开启一条新线程,然后所有的 block 任务 在这一条新线程中串行执行。

  • dispatch_async 函数提交到不同自定义串行队列中的 block 任务则会各自开启一条新线程并行执行。

  • dispatch_async 函数提交到并行队列中的多个 block 任务则会各自开启一条线程并行执行。

  • dispatch_sync 的话不管是提交到串行队列还是并行队列中的 block 任务,它们都是在当前线程中串行执行的,它不会开启新线程。但是这里有一个特殊点,就是如果队列是主队列的话,则 block 任务只能在主线程中执行,不管我们通过 dispatch_sync 或者 dispatch_async 方式提交,都是如此。即主队列中和主线程是绑定的,只要我们往主队列中提交任务,那么它必会在主线程中执行。当然主线程中也可以执行其他任意的我们自定义的队列中的任务。如下代码:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"👉👉 %@", [NSThread currentThread]);
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        // 这里是主线程。(也可以理解为在原始线程上启用了一条新线程 😂)
        NSLog(@"👉 %@", [NSThread currentThread]);
    });
    
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        // 这里用的 dispatch_sync 提交的,所以不开启线程,和上面的 👉👉 是同一条线程。
        NSLog(@"👉👉 %@", [NSThread currentThread]);
    });
});

 dispatch_continuation_s 结构体定义如下:

typedef struct dispatch_continuation_s {
    union {
        const void *do_vtable;
        uintptr_t dc_flags;
    };
    
    union {
        pthread_priority_t dc_priority;
        int dc_cache_cnt;
        uintptr_t dc_pad;
    };
    
    struct dispatch_continuation_s *volatile do_next; // 下一个任务
    struct voucher_s *dc_voucher;
    
    // typedef void (*dispatch_function_t)(void *_Nullable); // 函数指针
    
    // 要执行的函数指针
    //(使用 dispatch_async 函数的话,
    // 调用 _dispatch_Block_invoke(work) 函数,取出 block 的函数指针,
    // 将 work 的函数封装成 dispatch_function_t)
    
    dispatch_function_t dc_func; // 指向 block 结构体的函数指针
    
    void *dc_ctxt; // 方法的上下文(参数)(使用 dispatch_async 函数的话,此指针指向堆区中的 block)
    
    void *dc_data; // 相关数据
    void *dc_other; // 其它信息 
    
} *dispatch_continuation_t;
DISPATCH_ALWAYS_INLINE
static inline dispatch_qos_t _dispatch_continuation_init(dispatch_continuation_t dc,
        dispatch_queue_class_t dqu, dispatch_block_t work,
        dispatch_block_flags_t flags, uintptr_t dc_flags)
{

    ...
    // 在这里根据 dc_flags 判断是同步还是异步对 func 进行赋值
    dispatch_function_t func = _dispatch_Block_invoke(work);// 封装work - 异步回调(func 只是 block 的函数)
    
    if (dc_flags & DC_FLAG_CONSUME) {
        func = _dispatch_call_block_and_release; // 回调函数赋值 - 同步回调(func 是执行 block 后并释放 block)
    }
    
    return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}

dispatch_async 函数内部把 dispatch_continuation_s 结构体变量准备好后调用 _dispatch_continuation_async(dq, dc, qos, dc->dc_flags)

_dispatch_continuation_async 函数内部使用了一个宏定义:dx_push,宏定义的内容是调用 dqudispatch_queue_class_t)的 vtable 的 dq_push(dq_push 是一个函数指针,是作为 vtable 的属性存在的,那么它是何时进行赋值的呢?)。

 全局搜索 dq_push 发现存在多处不同的队列进行赋值,例如根队列(.dq_push = _dispatch_root_queue_push)、主队列(.dq_push = _dispatch_main_queue_push)、并发队列(.dq_push = _dispatch_lane_concurrent_push)、串行队列(.dq_push = _dispatch_lane_push)等等,当我们调用 dispatch_async 函数队列参数传入一个自定义并发队列时会执行 _dispatch_lane_concurrent_push

 大概是把 dc 放入队列中。然后在 _dispatch_root_queue_poke_slow 函数中:

  • 通过 _dispatch_root_queues_init 方法注册回调
  • 通过 do-while 循环创建线程,使用 pthread_create 方法

 进入 _dispatch_root_queues_init 源码实现,发现是一个 dispatch_once_f 单例,其中传入的 func 是 _dispatch_root_queues_init_once,进入 _dispatch_root_queues_init_once 的源码,其内部不同事务的调用句柄都是 _dispatch_worker_thread2。

 其 block 回调执行的调用路径为:_dispatch_root_queues_init_once ->_dispatch_worker_thread2 -> _dispatch_root_queue_drain -> _dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline -> _dispatch_client_callout -> dispatch_call_block_and_release,最后在线程池中取出的线程中通过 dx_invoke 执行 block 回调,每一个 dx_push 和 dx_invoke 配对调用的。

 大概过程可以理解为并行队列中的 block 被 pop 出来放在不同的线程中执行,并行队列在 pop block 出来的时候并不会等当前的 block 执行完成后才 pop 下一个 block 出来,而是说连续 pop,不管当前的 block 有没有执行完毕都会连续 pop 队列中的 block 出来在不同的线程中执行。


19. dispatch_sync 函数执行流程分析。

 首先我们要记得一点 dispatch_sync 函数不管是把 block 任务提交到自定义串行队列还是并行队列(主队列比较特殊,这里除外),它都不会开启一条新线程。

dispatch_sync -> _dispatch_sync_f -> _dispatch_sync_f_inline

_dispatch_sync_f_inline 函数实现中开局是一个 dq->dq_width == 1 的判断,队列创建中我们知道串行队列的 dq_width 值为 1,自定义的并发队列的 dq_width 值为 0xffeull,根队列的 dq_width 值是 0xfffull,然后接下来则是分别针对串行队列和并行队列的分支。

  • 如果 dq 参数是串行队列的话会执行 _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags) 函数。
  • 如果 dq 参数是并发队列的话,会执行 _dispatch_sync_invoke_and_complete 函数。

 我们先看一个并发队列的简单分支,追着源码看的话如果 dispatch_sync 函数调用传入的 dq 是并行队列的话,就是正常的执行:_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline 主要有三个步骤:

  • 将任务压入队列: _dispatch_thread_frame_push(&dtf, dq);
  • 执行任务 block: _dispatch_client_callout(ctxt, func);
  • 将任务出对列:_dispatch_thread_frame_pop(&dtf);

 从实现中可以看出,是先将任务 push 队列中,然后执行 block,在将任务 pop,所以任务是顺序执行的。

dispatch_queue_t custom_Queue = dispatch_queue_create("DISPATCH_QUEUE_SERIAL", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(custom_Queue, ^{
    NSLog(@"👉 %@", [NSThread currentThread]);
    dispatch_sync(custom_Queue, ^{
        NSLog(@"👉👉 %@", [NSThread currentThread]);
        dispatch_sync(custom_Queue, ^{
            NSLog(@"👉👉👉 %@", [NSThread currentThread]);
        });
        NSLog(@"👉👉👉👉 %@", [NSThread currentThread]);
    });
    NSLog(@"👉👉👉👉👉 %@", [NSThread currentThread]);
});

// 控制台打印:
👉 <NSThread: 0x60000348d200>{number = 5, name = (null)}
👉👉 <NSThread: 0x60000348d200>{number = 5, name = (null)}
👉👉👉 <NSThread: 0x60000348d200>{number = 5, name = (null)}
👉👉👉👉 <NSThread: 0x60000348d200>{number = 5, name = (null)}
👉👉👉👉👉 <NSThread: 0x60000348d200>{number = 5, name = (null)}

 如果把 custom_Queue 定义为串行队列的话,运行则会 crash,这里有一个很重要的点是因为在串行队列中一个 block 任务必须执行完成并出队后才能执行接下来的 block 任务,而并行队列则不是,它不用等前一个 block 必须执行完成后才能插入新的 block 任务执行,它可以把新插入的 block 执行完成并出队后,再接着执行前面未执行完成的 block 任务。(且此时都是在一条线程中完成的)

 我们接着看上面的,如果 dq 参数是串行队列的话会执行 _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags) 函数的分支。这里牵涉到死锁的知识点,如果未发生死锁时执行流程是和 dq 参数是并行队列的情况相同,这里我们只关注死锁。

 我们最常见的主线程死锁时会看到控制的函数调用栈停在了 __DISPATCH_WAIT_FOR_QUEUE__ 函数。

 死锁的本质是串行队列中任务的循环等待,同步函数中如果当前正在执行的队列和等待的是同一个队列,形成相互等待的局面,则会造成死锁。发生死锁的原因存放在 _dispatch_sync_f_slow 函数调用中。

_dispatch_sync_f_slow 函数中生成了一些任务的信息,然后通过 _dispatch_trace_item_push 来进行压栈操作,从而存放在我们的同步队列中(FIFO),从而实现函数的执行。

__DISPATCH_WAIT_FOR_QUEUE__ 函数中,它会获取到队列的状态,看其是否为等待状态,然后调用 _dq_state_drain_locked_by 中的异或运算,判断队列和线程的等待状态,如果两者都在等待,那么就会返回 YES,从而造成死锁的崩溃。

_dispatch_sync 首先获取当前线程的 tid,然后获取到系统底层返回的 status,然后获取到队列的等待状态和 tid 比较,如果相同,则表示正在死锁,从而崩溃。


20. dispatch_once 执行原理,它是如何保证在应用程序全局中只能执行一次的。

  dispatch_once 保证任务只会被执行一次,即使同时多线程调用也是线程安全的。常用于创建单例、swizzeld method 等功能。

dispatch_once 是同步函数,会阻塞当前线程,直到 block 执行完成后返回,才会执行接下的语句。

dispatch_once 不同于我们的日常的函数,它的 block 参数全局只能调用一次,即使在多线程的环境中也是全局只能执行一次,那么当多个线程同时调用 dispatch_once 时,系统是怎么加锁或者阻塞线程保证线程安全的呢?

#ifdef __BLOCKS__
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
    dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
#endif

dispatch_once_t 类型参数 val 作为 dispatch_once 函数一起使用的谓词,必须将其初始化为零,dispatch_once 正是根据 val 参数是否为 0 来判断是否执行提交的 block。(静态和全局变量默认为零)

dispatch_once_gate_t 是指向 dispatch_once_gate_s 结构体的指针,dispatch_once_gate_s 结构体内部仅包含一个联合体,在 dispatch_once 函数中传入的 val 参数会被强转为 dispatch_once_gate_t 类型。

typedef struct dispatch_once_gate_s {
    union {
        dispatch_gate_s dgo_gate;
        uintptr_t dgo_once;
    };
} dispatch_once_gate_s, *dispatch_once_gate_t;

DLOCK_ONCE_UNLOCKEDDLOCK_ONCE_DONE 对应,分别代表 dispatch_once 执行前后的标记状态。DLOCK_ONCE_UNLOCKED 用于标记 dispatch_once 还没有执行过,DLOCK_ONCE_DONE 用于标记 dispatch_once 已经执行完了。

#define DLOCK_ONCE_UNLOCKED   ((uintptr_t)0)
#define DLOCK_ONCE_DONE   (~(uintptr_t)0)

 当多条线程同时调用 dispatch_once_f 函数时内部的 _dispatch_once_gate_tryenter 函数原子性的判断 ldispatch_once_gate_t l = (dispatch_once_gate_t)val;)是否非零,非零表示 dispatch_once_f 提交的函数已经执行过了(或者正在执行),零的话还没有执行过。

 如果 ll->dgo_once)是零的话,_dispatch_once_gate_tryenter 函数内部也会把 ll->dgo_once) 赋值为当前线程的 ID(这里是一个临时赋值),在最后 dispatch_once_f 中提交的函数执行完成后 _dispatch_once_gate_broadcast 函数内部会把 ll->dgo_once)赋值为 DLOCK_ONCE_DONE。(_dispatch_lock_value_for_self 函数是取出当前线程的 ID)

 这里藏一个点,就是每次执行 _dispatch_once_gate_tryenter 函数时 ll->dgo_once)被赋值为当前线程的 ID,它对应了下面 _dispatch_once_gate_broadcast 函数内的 v == value_self 的判断,如果是单线程的调用 dispatch_once_f 的话,则是不存在其它线程被阻塞等待的,也就不需要线程唤醒的操作,而如果是多线程的环境下,_dispatch_once_gate_tryenter 函数会被不同线程调用,每次 v 都会被更新当前调用线程的 ID,而在 _dispatch_once_gate_broadcast 函数内部,value_self 的值是最初调用 dispatch_once_f 函数的线程的 ID,而 v 则是最后面调用 dispatch_once_f 函数的线程的 ID,且它们这些线程正在阻塞等待 dispatch_once_f 函数提交的函数执行完成,所以当 dispatch_once_f 提交的的函数执行完成后,需要对阻塞等待的线程进行唤醒操作。

_dispatch_lock_value_for_self 取出当前线程的 ID,用于赋值给 valdgo_once 成员变量)。( valdispatch_once_f 提交的函数执行完成之前会赋值为线程 ID,当提交的函数执行完成后会赋值为 DLOCK_ONCE_DONE,如我们为 dispatch_once 准备的 static dispatch_once_t onceToken;,在 dispatch_once 执行前打印 onceToken 值为 0,onceToken 初始值必须为 0,否则 dispatch_once 里的 block 不会执行,当 dispatch_once 执行完成后,打印 onceToken,它的值是 -1,如果我们手动把 onceToken 修改为 0,则可以再次执行 dispatch_once 提交的 block)。

_dispatch_once_wait 函数内部是用了一个 do while 循环来不断的读取 &dgo->dgo_once 的值,直到其被置为 DLOCK_ONCE_DONE,看到一些文章中说是用 _dispatch_thread_semaphore_wait 来阻塞线程,这里已经发生更新。这里在多线程的情况下仅是用了 do while 循环来进行阻塞等待,并没有涉及到线程唤醒等消耗资源的操作,提高了 dispatch_once 在多线程调用下的性能。

🎉🎉🎉 未完待续...