iOS GCD 多线程开发
GCD 是 iOS 中对多线程能力的一种封装,全名称为 Grand Central Dispatch。
基本概念
并行与并发
并行 parallelism,表示逻辑概念上的同时。并发 concurrency,表示物理概念上的同时。
- 并发执行:表示任务在同一时间段里被执行,可能是交替执行。
- 并行执行:表示任务同时被执行。
任务
任务:执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block中的。执行任务有两种方式:同步执行和异步执行。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
- 同步执行(sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。只能在当前线程中执行任务,不具备开启新线程的能力。
- 异步执行(async):异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力。
备注:异步执行虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
队列
队列:这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则.
Dispatch Queue 是 GCD 处理异步任务和并发任务的关键载体,在 GCD 中,将 task 放入某个 Dispatch Queue 中,然后等待系统去处理。
从功能来说,可以将 dispatch queue 划分为三类:
- Serial Dispatch Queue。每次只有一个任务被执行,实际上为单线程执行。用户可以根据需要创建任意多的串行队列,串行队列彼此之间是并发的。
- Concurrent Dispatch Queue。多个任务并发(同时)执行。global queue 是系统内置的一些不同优先级的并行队列,用户也可以根据需要自己创建并行队列。
- Main Dispatch Queue。所有放在主队列中的任务,都会放到主线程中执行。
备注:并发队列的并发功能只有在异步函数下才有效。
系统创建的 queue 是真实可以执行任务的,而用户创建的 queue 只起到一个控制任务什么时候可以提交给系统创建的的任务的作用,他们之间,就是依赖 target queue。用户创建的 dispatch queue,不论是并行队列还是串行队列,它们默认的 target queue 都是 QOS_CLASS_DEFAULT 类型的 global queue,也就是说最终它们都是要执行在 global queue 队列上。
GCD 的使用步骤
创建/获取队列
使用 dispatch_queue_create 来创建队列,需要传入两个参数,第一个参数表示队列的唯一标识符,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。
使用 dispatch_get_global_queue 来获取全局并发队列(Global Dispatch Queue)。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可。
1 | // 串行队列的创建方法 |
任务执行
GCD 提供了同步执行任务的创建方法dispatch_sync和异步执行任务创建方法dispatch_async。
1 | // 同步执行任务创建方法 |
GCD 的组合场景
根据队列的三种分类和任务两种执行方式,GCD 一共有六种组合使用情况。
执行方式 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 在同一个串行队列中对当前队列sync操作都会导致死锁 |
异步 | 有开启新线程,并发执行任务 | 有开启新线程,串行执行任务 | 如果在当前队列async,并不会开启新线程;在其他队列当中再对该串行队列进行asyn操作会开启新线程 |
同步执行 + 并发队列
特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
按顺序执行的原因:虽然并发队列可以同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
异步执行 + 并发队列
特点:可以开启多个线程,任务交替(同时)执行。
异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务
同步执行 + 串行队列
特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
异步执行 + 串行队列
特点:会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务。
同步执行 + 主队列
特点:同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会
异步执行 + 主队列
特点:只在主线程中执行任务,执行完一个任务,再执行下一个任务。
GCD 线程间的通信
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
GCD 常用方法
延时执行:dispatch_after
延时执行:用于在指定时间之后执行某个任务。
1 | int64_t delayInSeconds = 5.0; // 延迟的时间 |
注意:dispatch_after 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。
单次执行:dispatch_once
单次执行:用于创建单例、或者运行整个程序过程中只执行一次的代码。
1 | static dispatch_once_t onceToken; |
阻塞执行:dispatch_barrier
阻塞执行:用于分割两组异步执行任务。确保两组任务先、后完成。
dispatch_barrier_async 栅栏函数,会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
dispatch_barrier 的作用:
- 保证队列任务顺序执行
- 保证线程安全
注意点:必须使用自定义队列,不能使用全局并发队列。使用栅栏函数的意义就是阻塞队列里的任务,而系统的全局并发队列是并发的所以会产生冲突导致栅栏函数失效。
1 | dispatch_queue_t queue = dispatch_queue_create("com.xxx.xxxx", DISPATCH_QUEUE_CONCURRENT); |
队列组:dispatch_group
可以实现类似栅栏函数的效果:当 group 中所有的任务都执行完成后,方可执行后续任务。
GCD 队列组主要包括五个函数:
- dispatch_group_create 创建一个调度任务组
- dispatch_group_async 把一个任务异步提交到任务组里
- dispatch_group_enter/dispatch_group_leave 这种方式用在不使用dispatch_group_async来提交任务,且必须配合使用
- dispatch_group_notify 用来监听任务组事件的执行完毕
- dispatch_group_wait 设置等待时间,在等待时间结束后,如果还没有执行完任务组,则返回。返回0代表执行成功,非0则执行失败
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
迭代执行:dispatch_apply
迭代执行:用于按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
- 在串行队列中使用 dispatch_apply,就和 for 循环一样,按顺序同步执行。
- 在并发队列中使用 dispatch_apply,则每个循环并发执行。
注意:无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕。
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
信号量机制:dispatch_semaphore
信号量机制主要是通过设置有限的资源数量来控制线程的最大并发数量以及阻塞线程实现线程同步等 。
GCD 信号量主要包括三个函数:
- dispatch_semaphore_create 用来创建一个semaphore信号量并设置初始信号量的值;
- dispatch_semaphore_signal 发送一个信号让信号量增加1(对应PV操作的V操作);
- dispatch_semaphore_wait 等待信号使信号量减1(对应PV操作的P操作)。
Dispatch Semaphore 主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务。
- 保证线程安全,为线程加锁。
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
参考文档
https://zhangbuhuai.com/post/gcd-basics.html
https://juejin.cn/post/6926107768001101838