GCD(Grand Central Dispatch),是有Apple公司开发的一个多核编程的解决方案,用以优化应用程序支持多核处理器,是基于线程模式之上执行并发任务。
一、GCD的基本概念
GCD是纯C语言的,提供了非常多并且强大的函数,比NSThread
性能更好。
1.1. GCD的优势
- 利用设备多核进行并行运算
- 自动充分利用设备的CPU内核
- 自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
1.2. 任务和队列
GCD中有2个核心的概念:
- 任务: 执行什么操作
- 队列: 用来存放任务
GCD会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的FIFO(First In First Out)原则(先进先出,后进后出)。
1.2.1. 执行任务
执行任务有两种方式:同步和异步。
同步: 只能在当前线程中执行任务,不具备开启新线程的能力。
1 | /* |
异步: 可以在新的线程中执行任务,具备开启新线程的能力。
1 | /* |
1.2.2. 队列
队列的类型:并行和串行。
并行: 可以让多个任务同时执行(自动开启多个线程同时执行任务),并发功能只有在异步函数下才有效。
串行: 顺序执行任务(一个任务执行完毕后,再执行下一个任务)。
创建队列有两种方式:
- 创建队列
1
2
3
4
5
6
7
8
9
10// 定义
/*
* label:队列唯一标识符(一般格式:BundleID+功能描述)
* attr:串行或并行(并行:DISPATCH_QUEUE_CONCURRENT,串行:DISPATCH_QUEUE_SERIAL)
*/
dispatch_queue_create(const char *_Nullable label,
dispatch_queue_attr_t _Nullable attr);
// 使用
dispatch_queue_t queue = dispatch_queue_create("com.idbeny.www.download", DISPATCH_QUEUE_CONCURRENT);
- 获取全局队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 定义
/*
* identifier:优先级(一般传默认优先级)
* flags:预留参数,传0即可
*/
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);
// 优先级(宏)
// 使用
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- 获取主队列
主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会在主线程中串行执行。1
2
3
4
5// 定义
dispatch_get_main_queue(void)
// 使用
dispatch_queue_t queue = dispatch_get_main_queue();
二、同步、异步、并发、串行
同步、异步、并行、串行。这四个专业术语非常容易混淆。
同步串行: 队列中的线程依次执行,并且主线程阻塞,等待任务的完成。
异步串行: 队列中的线程依次执行,同时主线程还在继续执行。
同步并行: 队列中的线程会一起执行,但是同一时段只能有一个线程执行其他线程等待,等所有任务执行完,主线程继续执行。
异步并行: 队列中的线程一起执行,主线程也会继续执行。
同步和异步区别: 是否能开启新的线程。
串行和并行区别: 执行的表现形式不同。串行是依次执行,只有当前线程结束之后,另一个线程才开启。而并行是所有任务一起执行。
2.1. 异步并行
1 | - (void)asyncConcurrent { |
异步并行,开启了多条子线程,并且任务没有按照顺序执行。
上面示例开了5个异步任务后GCD自动开启了5个子线程来执行任务,如果开启100个任务是不是会创建100个子线程?肯定不会的,开几条线程并不是任务的数量决定的,是GCD内部自动决定的。假如真的开了100个线程,估计CPU也会因此罢工。
把上面的示例代码修改为串行执行,看下任务执行顺序:
2.2. 异步串行
1 | - (void)asyncSerial { |
异步串行,开启一条子线程,所有任务都在该线程顺序执行。
2.3. 同步并行
1 | - (void)syncConcurrent { |
同步并行,没有开启子线程,所有任务都在主线程顺序执行。
2.4. 同步串行
1 | - (void)syncSerial { |
同步串行,不会开启子线程,所有任务都在主线程顺序执行。
2.5. 主队列
示例代码(异步):
1 | - (void)asyncMain { |
异步任务,所有任务都在主线程串行执行。
示例代码(同步):
1 | - (void)syncMain { |
同步任务,直接进入死锁,最终程序崩溃。
造成死锁的原因是:主线程在执行dispatch_sync
函数的时候,由于任务需要同步执行,主队列让主线程先执行队列中的任务,这时候dispatch_sync
函数还没有执行完,主线程不知道该执行哪个就造成了死锁。
解决死锁:把syncMain
整个代码块放到子线程中执行。
1 | [self performSelectorInBackground:@selector(syncMain) withObject:nil]; |
三、GCD的基本使用
3.1. 线程通信
GCD线程通信使用起来比NSThread
更加方便。
示例代码(下载图片):
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
3.2. 单例模式
GCD提供dispatch_once
函数,可以让代码在整个程序运行过程中只运行一次(一般应用在单例模式)。
关键代码:
1 | static dispatch_once_t onceToken; |
内部实现原理:静态变量由于内存地址是全局唯一的,每次程序运行到此处时会查看静态变量内存地址是否有值(判断是否为0),如果有值就不再执行,否则就会执行代码块。
单例模式可以保证在程序运行过程中,一个类只有一个实例(节约系统资源),但是单例多了也会造成浪费系统资源。
示例代码:
1 | // DBDownloadManager.h |
还有一种简洁式写法(偷懒):
1 | + (instancetype)manager { |
这种写法有风险,如果用户通过alloc、copy、mutableCopy
创建实例对象,对象的内存地址是不一样的。
3.3. 延迟执行
NSTimer
和NSObject(NSDelayedPerforming)
都可以让代码延迟执行,GCD也有提供延迟执行函数dispatch_after
。
1 | /* |
示例代码:
1 | NSLog(@"1"); |
注意:
dispatch_after
是等延迟时间到了之后再把任务提交到队列(如果先提交到队列,就很难控制队列执行的时间)。
3.3. 快速迭代
GCD有提供类似for
循环的迭代函数dispatch_apply
。
1 | /* |
示例代码:
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
通过输出,发现GCD迭代函数会开启多条子线程和主线程一起执行任务。
如果把上面示例代码中的队列换成串行队列,和for
循环就一样了。但是一定不能换成主队列,会形成死锁。
3.4. 栅栏
栅栏函数在实际开发中的应用场景主要是等待,比如网络请求C必须等到网络请求A和B请求完成后才能开始请求。
示例代码:
1 | dispatch_queue_t queue = dispatch_queue_create("com.idbeny.www.test", DISPATCH_QUEUE_CONCURRENT); |
上面示例代码是很常见的异步并行,任务的执行是没有顺序的。有没有办法让任务1和任务2执行完成后再执行任务3和任务4?答案:使用栅栏函数。
栅栏函数:
1 | dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); |
示例代码(使用栅栏):
1 | dispatch_queue_t queue = dispatch_queue_create("com.idbeny.www.test", DISPATCH_QUEUE_CONCURRENT); |
把栅栏放到对应位置,就会把相关任务一分为二,先执行栅栏前面的任务,后执行栅栏后面的任务。需要注意的是,栅栏本身不影响任务是并行还是串行。
警告:使用栅栏函数时,不能使用全局队列,否则会导致栅栏无效(栅栏会像同步/异步函数一样被放到队列中)。
3.5. 队列组
队列组是用来管理队列的,通过队列组可以在所有队列的任务完成后,再执行其他任务。
如果是一个队列,是可以通过栅栏函数实现基本功能的。如果是多个队列,栅栏函数就无法满足需求。
示例代码:
1 | dispatch_group_t group = dispatch_group_create(); |
队列组可以监听到所有队列执行完成的通知。dispatch_group_notify
是异步执行的。
注意:
dispatch_group_notify
函数传入的队列参数指的是后面的代码块放到哪个线程中执行。如果是自定义的队列就在子线程中执行,如果是主队列就在主线程中执行。
dispatch_group_notify
是如何知道队列组中任务执行状态的?
dispatch_group_async
是下面代码的合成写法:
1 | dispatch_group_enter(group); |
dispatch_async
必须放在dispatch_group_enter
后面,在任务执行完成后必须执行dispatch_group_leave
,否则dispatch_group_notify
收不到通知。dispatch_group_enter
和dispatch_group_leave
是成对使用。
3.6. 补充
dispatch_queue_create
和dispatch_get_global_queue
的区别:
dispatch_get_global_queue
在整个应用程序中本身是默认存在的,并且提供了优先级。dispatch_queue_create
是开发人员从0开始去创建一个队列。在iOS6.0之前,GCD中只要使用了带
create
和retain
的函数在最后都要做release
操作(防止内存泄漏),除了主队列和全局并发队列。ARC
之后,就不需要手动释放了。使用栅栏函数时,官方明确规定只有和
dispatch_queue_create
创建的队列一起使用才有效(没有具体原因)。
参考资料: