欢迎阅读iOS底层系列(建议按顺序)
本文概述
本文主要在列出GCD
使用过程中常用的API
,在使用场景和注意细节加以说明。
1.多线程之GCD
1.1 为什么是GCD
线程编程有多种方式,pthread
,NSThread
,GCD
,NSOperation
。为什么选择GCD
?
方案 | 简介 | 语言 | 生命周期 |
---|---|---|---|
pthread | 通用API,跨平台,使用难度大 | C | 程序员管理 |
NSThread | 面向对象,简单易用,可直接操作线程 | OC | 程序员管理 |
GCD | 充分利用设备的多核 | C | 自动管理 |
NSOperation | 面向对象,基于GCD,功能更多 | OC | 自动管理 |
GCD
是苹果为多核的并行运算提出的解决方案,它是一套纯C
语言的API
,它会自动利用更多的CPU
内核,会自动管理线程的生命周期
(创建线程、调度任务、销毁线程)。
管理线程的生命周期
是个棘手的活,而且容易出错,因此应尽可能避免使用。所以GCD
和基于GCD
的NSOperation
是日常开发中多线程的首选,程序员只需要告诉 GCD
想要执行什么任务,不需要编写任何线程管理代码。
关于GCD
和NSOperation
的选择:
NSOperation
是对线程的高度抽象,子类化NSOperation
的设计思路,是具有面向对象的优点,且能自定义实现特殊需求,使得实现是多线程支持和接口简单的,会使项目的程序结构更好,建议在复杂项目中使用。
GCD
本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而 Block 参数的使用,会是代码更为易读,建议在简单项目中使用。
1.2 如何使用GCD
一句话概括,将任务添加到队列,并且指定执行任务的函数。这是使用GCD
的核心思想。
这句话涉及任务
、队列
、函数
三个概念:
任务
:就是block
块内的代码,要被执行的部分队列
:执行任务的等待队列,即用来存放任务的队列函数
:是否可以在新的线程中执行任务,是否具备开启新线程的能力
1.3 队列和函数
串行队列(Serial):
- 不开辟新线程,每次只有一个任务被执行,任务一个接着一个地执行。
并发队列(Concurrent):
- 可以开启多个线程,让多个任务并发(同时)执行。
主队列(Main):
- 在主线程执行任务的队列,代码默认都被添加到主队列
- 本质上是
串行
队列
全局队列(Global):
- 系统自身使用的队列,如果对队列没有特殊要求,在执行异步任务时,可以借用
- 本质上是
并发
队列
同步函数(sync):
同步
添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行- 只能在当前线程中执行任务,不具备开启新线程的能力
异步函数(async):
异步
添加任务到指定的队列中,不做任何等待,可以继续执行任务- 可以在新的线程中执行任务,具备开启新线程的能力
不同队列和不同函数的组合在不同线程具有不同的效果。
1.4 队列和函数的组合
全局队列
类似于并发队列
,不额外分析,所以有以下6种
排列:
1.同步函数 + 串行队列
2.同步函数 + 并发队列
3.异步函数 + 串行队列
4.异步函数 + 并发队列
5.同步函数 + 主队列
6.异步函数 + 主队列
在主线程
下不同队列
和不同函数
的组合效果:
函数\队列 | 串行队列 | 并发队列 | 主队列 |
---|---|---|---|
同步函数 | 没有开启线程,串行执行任务 | 没有开启线程,串行执行任务 | 死锁 |
异步函数 | 有开启线程(1条),串行执行任务 | 有开启线程,并发执行任务 | 没有开启新线程,串行执行任务 |
同步函数
不会开启线程,且是串行
执行任务异步函数
不一定会开启线程,参考异步主队列
主线程
中,同步主队列
会产生死锁
,因为主队列中追加的同步任务和主线程本身的任务两者之间相互等待,阻塞了主队列
同步主队列
不一定会产生死锁
,比如在其他子线程
死锁
不一定只在主队列
,其他的串行队列
也可能
关于这几点后面的代码部分会进行验证。
基于同步并发
和异步串行
可能使用较少,理解相对困难些,详细说明下:
同步并发
是按顺序执行任务的但不开启线程。虽然并发队列
可以同时执行多个任务,但同步函数
不具备开启线程的能力,也就不会创新新线程,任务只能在一个线程执行。同一线程中只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作。所以就不存在并发,任务只能一个接一个按顺序执行,不能同时被执行。
异步串行
是按顺序执行任务的且会开启一条线程。因为异步函数
具备开启新线程的能力,但串行队列
每次只有一个任务被执行,任务要一个接一个按顺序执行,使得其只开启一个线程。
简单来说,
同步并发因为没有开启新线程,导致任务都在一个线程执行,并发失去效果,任务按顺序执行。
异步串行虽然开启了一个线程,但因为是串行,任务还是按顺序执行。
虽然同步并发
和异步串行
的任务都是按顺序进行,但原因有些区别,同步并发
在于同步
,异步串行
在于串行
。
2. API的使用说明
2.1 dispatch_block_t
① dispatch_block_t 创建任务
② dispatch_queue_t 将任务添加到队列
③ dispatch_async 指定执行任务的函数
这是最能体现将任务添加到队列,并且指定执行任务的函数思想的三行代码。
不过一般合并表示:
dispatch_async(dispatch_queue_create("com.juejin.cn", NULL), ^{
NSLog(@"hello word");
});
2.2 dispatch_queue_t
- 主队列:使用
dispatch_get_main_queue()
获取,在主线程
中同步主队列
会造成死锁
- 全局队列:使用
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
获取,第一个参数为优先级,默认为DISPATCH_QUEUE_PRIORITY_DEFAULT
,优先级如下:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
- 串行队列:使用
dispatch_queue_create("...",DISPATCH_QUEUE_SERIAL)
创建,或者dispatch_queue_create("...",NULL)
亦可
#define DISPATCH_QUEUE_SERIAL NULL
- 并发队列:使用
dispatch_queue_create("...",DISPATCH_QUEUE_CONCURRENT)
创建
2.3 dispatch_sync和dispatch_async
① 同步 + 串行:
dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
------------------------------------------------------------
2020-12-09 16:01:41.064806+0800 001---函数与队列[26946:1461774] 0-<NSThread: 0x2801ef080>{number = 1, name = main}
2020-12-09 16:01:41.064905+0800 001---函数与队列[26946:1461774] 1-<NSThread: 0x2801ef080>{number = 1, name = main}
2020-12-09 16:01:41.064951+0800 001---函数与队列[26946:1461774] 2-<NSThread: 0x2801ef080>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行
② 同步 + 并发:
dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
------------------------------------------------------------
2020-12-09 16:06:04.335006+0800 001---函数与队列[27053:1463520] 0-<NSThread: 0x283627040>{number = 1, name = main}
2020-12-09 16:06:04.335107+0800 001---函数与队列[27053:1463520] 1-<NSThread: 0x283627040>{number = 1, name = main}
2020-12-09 16:06:04.335153+0800 001---函数与队列[27053:1463520] 2-<NSThread: 0x283627040>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行
③ 异步 + 串行:
dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
dispatch_async(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
------------------------------------------------------------
2020-12-09 16:09:56.688148+0800 001---函数与队列[27187:1465347] 0-<NSThread: 0x282435900>{number = 6, name = (null)}
2020-12-09 16:09:56.688234+0800 001---函数与队列[27187:1465347] 1-<NSThread: 0x282435900>{number = 6, name = (null)}
2020-12-09 16:09:56.688281+0800 001---函数与队列[27187:1465347] 2-<NSThread: 0x282435900>{number = 6, name = (null)}
...
------------------------------------------------------------
开启一条线程,顺序执行
④ 异步 + 并发:
dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
dispatch_async(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
------------------------------------------------------------
2020-12-09 16:12:47.086771+0800 001---函数与队列[27221:1466848] 0-<NSThread: 0x2827d8b40>{number = 6, name = (null)}
2020-12-09 16:12:47.086875+0800 001---函数与队列[27221:1466848] 2-<NSThread: 0x2827d8b40>{number = 6, name = (null)}
2020-12-09 16:12:47.086931+0800 001---函数与队列[27221:1466849] 1-<NSThread: 0x2827f74c0>{number = 4, name = (null)}
...
------------------------------------------------------------
开启多条线程,乱序执行
⑤ 同步 + 主队列:
在主线程
中,同步主队列
会产生死锁
的结论得以验证。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
});
------------------------------------------------------------
2020-12-09 16:18:29.902827+0800 001---函数与队列[27272:1468868] 0-<NSThread: 0x282d0a200>{number = 1, name = main}
2020-12-09 16:18:29.912577+0800 001---函数与队列[27272:1468868] 1-<NSThread: 0x282d0a200>{number = 1, name = main}
2020-12-09 16:18:29.913928+0800 001---函数与队列[27272:1468868] 2-<NSThread: 0x282d0a200>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行
在子线程
,同步主队列
不会产生死锁
的结论得以验证。
⑥ 异步 + 主队列:
for (int i = 0; i<20; i++) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
------------------------------------------------------------
2020-12-09 16:31:28.193584+0800 001---函数与队列[27289:1470863] 0-<NSThread: 0x2818aaec0>{number = 1, name = main}
2020-12-09 16:31:28.193679+0800 001---函数与队列[27289:1470863] 1-<NSThread: 0x2818aaec0>{number = 1, name = main}
2020-12-09 16:31:28.193725+0800 001---函数与队列[27289:1470863] 2-<NSThread: 0x2818aaec0>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行
主队列
的任务都在主线程
执行,所以异步函数
也不一定会开启线程
的结论得以验证。
⑦ (同异步)串行 + 同步串行
无论在子线程
还是主线程
,向同一串行队列
追加同步串行
,都会产生死锁
。这是因为串行队列中追加的任务和串行队列中原有的任务两者产生互相等待。
死锁
不一定只在主队列
,其他的串行队列
也可能的结论得以验证。
2.4 dispatch_barrier_async 和 dispatch_barrier_sync
类似同步函数
,栅栏函数
也能起到同步
的效果。
同步
的效果就意味着栅栏函数
一般是搭配异步并发
使用。
当使用dispatch_barrier_async
时可得到以下结构:
dispatch_async(concurrentQueue, ^{
子线程任务A
});
dispatch_barrier_async(concurrentQueue), ^{
});
主线程任务A
dispatch_async(concurrentQueue, ^{
子线程任务B
});
输出顺序为:主线程任务A -> 子线程任务A -> 子线程任务B
当使用dispatch_barrier_sync
时可得到以下结构:
dispatch_async(concurrentQueue, ^{
子线程任务A
});
dispatch_barrier_sync(concurrentQueue), ^{
});
主线程任务A
dispatch_async(concurrentQueue, ^{
子线程任务B
});
输出顺序为:子线程任务A -> 主线程任务A -> 子线程任务B
根据结果,可以得到以下结论:
- 对于
同一队列
内的任务,能起到控制任务执行顺序,同步
作用 dispatch_barrier_sync
阻塞队列
也阻塞线程
,dispatch_barrier_async
只阻塞队列
注意细节:
异步栅栏函数
阻塞的是队列,专队专用
,非栅栏内的队列不会被阻塞。比如阻塞AFNetworking
网络请求时往往不能成功,因为其内部的队列是自行创建的,需要获取它的队列才能阻塞全局队列
的作用类似于并发队列
,如果使用栅栏函数
阻塞全局队列
,在进行耗时任务时会导致程序崩溃。因为全局队列
是系统使用的队列,app使用时也有很多系统任务在执行,阻塞系统队列会造成很多未知的后果
对栅栏函数
的相关特性做个总结:
- 同步作用
- 阻塞队列
- 可选阻塞线程
- 专队专用
- 不用全局队列
2.5 dispatch_group_t
dispatch_group_t
是提交到队列用于异步调用的一组块对象,内部维护未完成的关联任务的计数,在关联新任务时减少计数,在任务完成时增加计数,可以使用该计数来允许应用程序确定与组关联的所有任务何时完成。所以可以把相关的任务归并到一个组内来执行,通过监听组内所有任务的执行情况来做相应处理。
日常开发中,基于栅栏函数
控制网络请求的顺序较麻烦,使用dispatch_group_t
是可行且常见的方式。
dispatch_group_t
相关的API
包括:
dispatch_group_create:
- 创建可以将块对象分配给它的新调度组。
dispatch_group_async:
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
- 异步调度一个块以在指定队列执行,并同时将其与指定的调度组关联
dispatch_group_enter:
void dispatch_group_enter(dispatch_group_t group);
- 增加对应调度组中的未完成的关联任务,执行一次,任务计数减1(源码是-1)
dispatch_group_leave:
void dispatch_group_leave(dispatch_group_t group);
- 与
dispatch_group_enter
对应,减少对应调度组中的未完成的关联任务,执行一次,任务计数加1(源码是+1)。当计数为0时,dispatch_group_wait
解除阻塞和dispatch_group_notify
的block执行
dispatch_group_wait:
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
- 会阻塞当前线程,等待调度组完成。当调度组完成时返回值为0或超时时间到达时返回值不为0,都会解除阻塞当前线程
dispatch_group_notify:
void dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue, dispatch_block_t block);
- 与
dispatch_group_wait
类似,调度组执行完毕时响应,但不会阻塞线程
注意细节:
-
dispatch_group_notify
自动控制任务计数,dispatch_group_enter
和dispatch_group_leave
是手动控制任务计数。所以dispatch_group_notify
使用更方便,dispatch_group_enter
和dispatch_group_leave
更适合复杂环境。 -
dispatch_group_enter
和dispatch_group_leave
需要成对出现,可先enter
多次,但也要对应次数的leave
。enter
多于leave
会导致notify
不执行,leave
多于enter
会导致程序崩溃。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue1 = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog(@"第一个任务");
});
dispatch_group_async(group, queue, ^{
dispatch_async(queue1, ^{
NSLog(@"第二个任务");
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"两个任务都完成了");
});
-----------------------------------------------------------------
以上代码按认知来说,顺序应是第一个任务 -> 第二个任务 -> 两个任务都完成了
-----------------------------------------------------------------
实际情况:
2021-02-04 09:28:22.897517+0800 005---GCD进阶[11694:2915771] 第一个任务
2021-02-04 09:28:22.897753+0800 005---GCD进阶[11694:2915620] 两个任务都完成了
2021-02-04 09:28:22.897755+0800 005---GCD进阶[11694:2915769] 第二个任务
特别注意:
- 当组内任务是在
子线程
执行时,dispatch_group_notify
在任务未完成时就会执行,此时需要手动控制计数防止出错
2.6 dispatch_once_t
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_once_t
在整个app运行期间只执行一次(理论如此),默认onceToken
为0,执行一次后为-1
注意细节:
dispatch_once_t
不是一定只执行一次,如果需要再次执行则需要设置onceToken
为0
使用场景:
- 单例的创建,保证只创建一次
Method Swizzling
黑魔法,防止方法又被交换回来- 固定UI的构建阶段
2.7 dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
此函数将等待到指定的时间,然后异步地向指定队列添加任务。
使用场景:
- 自动消失的弹窗
- 防止按钮重复点击
- 其他需要延时执行的场景
2.8 dispatch_semaphore_t
dispatch_semaphore_create:
dispatch_semaphore_t dispatch_semaphore_create(long value);
- 创建
dispatch_semaphore_t
类型的信号量,创建的时候需要指定信号量的大小,且必须大于0
dispatch_semaphore_wait:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
该函数会对信号量做减1操作。如果减1后的值小于0,会一直等待,阻塞当前线程。
dispatch_semaphore_signal:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
该函数会对信号量做加1操作。
注意细节:
dispatch_semaphore_wait
和dispatch_semaphore_signal
是成对出现的。- 创建信号时,取值可以大于1,起到控制最大并发数的作用
dispatch_semaphore_t
也是锁的一种形式
使用场景:
- 同步
- 线程安全加锁
- 控制最大并发数
2.9 dispatch_source_t
一种协调特定底层系统事件(如文件系统事件、计时器和UNIX信号)处理的对象。
在任一线程上调用它的一个函数dispatch_source_merge_data
后,会执行
Dispatch Source
事先定义好的句柄,这个过程叫Custom event
用户事件,是 dispatch source
支持处理的一种事件。
dispatch_source_create:
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t _Nullable queue)
- 创建一个新的调度源来监控底层系统对象,并自动地向调度队列提交处理程序块以响应事件
dispatch_source_set_event_handler:
void dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler)
- 创建事件源的回调
dispatch_source_merge_data:
void dispatch_source_merge_data(dispatch_source_t source, unsigned long value)
- 将数据合并到分派源并将其事件处理程序块提交到其目标队列。可以使用此函数来指示在一个应用程序定义的
DISPATCH_SOURCE_TYPE_DATA_ADD
或DISPATCH_SOURCE_TYPE_DATA_OR
类型的分派事件源上发生了事件。
dispatch_source_get_data:
unsigned long dispatch_source_get_data(dispatch_source_t source)
- 获取源事件数据
dispatch_resume:
void dispatch_resume(dispatch_object_t object)
- 继续源
dispatch_suspend:
void dispatch_suspend(dispatch_object_t object)
- 挂起源
使用场景:
- 基于源的定时器
- 监听数据的改变
3.写在后面
GCD
的API
远不止这些,以上仅列出了常用的部分。在使用API的过程中,如果知道其底层实现,用起来会更得心应手。
下一章是多线程系列之底层原理篇。敬请关注。