GCD简介
iOS开发中多线程的API主要有pthread,NSThread,NSOperation和GCD,前两者在现在开发过程中已经不常用,NSOperation是面向对象封装的一套API,而GCD则是一套纯C语言API。
引用下百度的介绍
GCD为Grand Central Dispatch的缩写。
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。
GCD有哪些优点:
- 提供了易于使用的并发模型而不仅仅只是锁和线程,可以让我们轻松的进行多线程编程。
- 自动利用更多的CPU内核。
- 自动管理线程的生命周期,以及多线程调度。
- 提供了简单创建单例的接口等等。
Dispatch_Queue(队列)概念
队列
GCD中的队列和数据结构中的队列特性上一致,都是受限制的线性表,遵循FIFO(First In First Out),即新的任务需要在队尾加入,读取任务则从队首部读取,即先进先出
队列又分为:
- 串行队列(Serial Dispatch Queue)
简单理解串行队列就是按顺序执行一个任务,上一个任务执行完毕后执行下一个任务,现实中的例子,去银行办业务,只有一个窗口,大家有秩序的排队办业务,即是串行队列 - 并发队列(Concurrent Dispatch Queue)
简单理解并发队列就是同时执行多个任务,现实中的极端例子,去银行排队办业务,有多个窗口同时可以办业务,即是并发队列,需要注意的是,这些任务会按照被添加的顺序依次开始执行。但是任务完成的顺序是任意的。
这里提到了“任务”的概念:
“任务”,在 GCD 里指的是 Block,即一段需要执行的代码块
任务执行方式有两种:
同步执行(dispatch_sync)
完成任务后才会返回,进行下一任务,可见同步不具备开启线程能力,只会在当前线程依次执行异步执行(dispatch_async)
完成任务后立即返回,进行下一任务,具备多线程能力
总结:并发队列只会在异步执行下生效,同步执行不会触发多线程创建。
GCD队列编程实现
创建一个队列
1 | //串行队列 |
dispatch_queue_create
创建一个dispatch_queue_t
队列,第一个参数设置该队列标识符,用于调试使用,第二个参数,则是队列类型DISPATCH_QUEUE_SERIAL
串行队列DISPATCH_QUEUE_CONCURRENT
并发队列
除了自己创建队列之外,系统提供了两个队列供我们获取使用
1 | //主线程队列 |
- 主线程队列:这个是主线程的串行队列
- 全局队列: 这是个全局的并发队列,很多时候可以不需要自己创建并发队列,直接获取全局队列即可 第一个参数为优先级,这是个大概优先级设置
1 | DISPATCH_QUEUE_PRIORITY_HIGH //高优先级 |
接下来即可在队列中添加任务执行
1 | //异步执行 |
这里总共有三种类型队列:
- 并发队列(包括全局队列)
- 串行队列
- 主线程队列
三种队列分别可以同步异步执行就有6种组合方式
- 主线程同步执行
- 主线程异步执行
- 串行队列同步执行
- 串行队列异步执行
- 并行队列同步执行
- 并行队列异步执行
接下来我们依次看下每种情况
主线程同步执行
1 | NSLog(@"Begin"); |
运行下我们会发现
2017-05-03 16:50:31.542 GCDDemo[41489:596607] Begin
只打印了begin,并没有执行下去
事实上这里发生了死锁,
之前说过dispatch_sync
会等待上一个任务执行完才会执行下一个任务,sync添加的任务需要执行,需要等待NSLog(@"End");
执行完毕,NSLog(@"End");
任务本身也添加在主线程队列中,所以执行这个任务的前提是sync添加的任务执行完毕,这就出现了死锁,两个任务互相等待
总结:
- 主线程中执行同步任务会发生死锁
- 串行队列中嵌套串行队列任务会发生死锁(这个留给大家自己验证,本质上和主线程同步发生死锁一致)
主线程异步执行
1 | NSLog(@"Begin"); |
运行结果
2017-05-03 17:09:26.031 GCDDemo[42671:613068] Begin
2017-05-03 17:09:26.031 GCDDemo[42671:613068] End
2017-05-03 17:09:26.042 GCDDemo[42671:613068] 0———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.043 GCDDemo[42671:613068] 1———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.044 GCDDemo[42671:613068] 2———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.052 GCDDemo[42671:613068] 0———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.057 GCDDemo[42671:613068] 1———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.059 GCDDemo[42671:613068] 2———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.060 GCDDemo[42671:613068] 0———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.062 GCDDemo[42671:613068] 1———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
2017-05-03 17:09:26.063 GCDDemo[42671:613068] 2———主线程异步执行<NSThread: 0x60000007d5c0>{number = 1, name = main}
总结:
主线程队列异步执行不会开辟线程,会在当前线程同步执行。
串行队列同步执行
1 | NSLog(@"Begin"); |
运行结果如下:
2017-05-03 17:25:48.225 GCDDemo[43694:625941] Begin
2017-05-03 17:25:48.225 GCDDemo[43694:625941] 0———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.226 GCDDemo[43694:625941] 1———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.226 GCDDemo[43694:625941] 2———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.226 GCDDemo[43694:625941] 0———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.227 GCDDemo[43694:625941] 1———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.227 GCDDemo[43694:625941] 2———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.227 GCDDemo[43694:625941] 0———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.228 GCDDemo[43694:625941] 1———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.228 GCDDemo[43694:625941] 2———串行队列同步执行<NSThread: 0x60800007f880>{number = 1, name = main}
2017-05-03 17:25:48.228 GCDDemo[43694:625941] End
总结: 串行队列异步执行不会开辟多线程,只会在一条线程中依次执行
串行队列异步执行
1 | NSLog(@"Begin"); |
运行结果如下
2017-05-03 17:20:16.514 GCDDemo[43349:621375] Begin
2017-05-03 17:20:16.514 GCDDemo[43349:621375] End
2017-05-03 17:20:16.515 GCDDemo[43349:621680] 0———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.515 GCDDemo[43349:621680] 1———串行队列同步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.515 GCDDemo[43349:621680] 2———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.516 GCDDemo[43349:621680] 0———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.516 GCDDemo[43349:621680] 1———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.516 GCDDemo[43349:621680] 2———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.516 GCDDemo[43349:621680] 0———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.517 GCDDemo[43349:621680] 1———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
2017-05-03 17:20:16.517 GCDDemo[43349:621680] 2———串行队列异步执行<NSThread: 0x608000268040>{number = 3, name = (null)}
总结: 串行队列异步执行不会开辟多线程,只会在一条线程中依次执行
细心的同学会发现串行队列同步和异步执行都没有开辟多线程,在一条线程中同步执行,那么对于串行队列同步和异步执行有什么区别呢?
区别只有一点:
dispatch_async:不会阻塞当前队列,立即返回添加当前队列后面任务,可以看到上图打印结果,先打印end。再打印async任务
dispatch_sync:会阻塞当前队列,等该sync任务全部执行完毕之后再添加当前队列后面任务,可以看到上图打印结果,先打印完sync任务打印end。
并发队列同步执行
1 | NSLog(@"Begin"); |
运行结果如下:
2017-05-03 17:34:24.407 GCDDemo[44222:633000] Begin
2017-05-03 17:34:24.407 GCDDemo[44222:633000] 0———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.407 GCDDemo[44222:633000] 1———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.408 GCDDemo[44222:633000] 2———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.408 GCDDemo[44222:633000] 0———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.408 GCDDemo[44222:633000] 1———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.408 GCDDemo[44222:633000] 2———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.408 GCDDemo[44222:633000] 0———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.408 GCDDemo[44222:633000] 1———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.409 GCDDemo[44222:633000] 2———并发队列同步执行<NSThread: 0x60800006f380>{number = 1, name = main}
2017-05-03 17:34:24.410 GCDDemo[44222:633000] End
总结: 并发队列同步执行不会开辟多线程,只会在一条线程中依次执行
并发队列异步执行
1 | NSLog(@"Begin"); |
运行结果如下
2017-05-03 17:38:10.893 GCDDemo[44461:636216] Begin
2017-05-03 17:38:10.894 GCDDemo[44461:636216] End
2017-05-03 17:38:10.894 GCDDemo[44461:636252] 0———并发队列异步执行<NSThread: 0x60800007a700>{number = 4, name = (null)}
2017-05-03 17:38:10.894 GCDDemo[44461:636254] 0———并发队列异步执行<NSThread: 0x60800007ac40>{number = 5, name = (null)}
2017-05-03 17:38:10.894 GCDDemo[44461:636251] 0———并发队列异步执行<NSThread: 0x60800007a800>{number = 3, name = (null)}
2017-05-03 17:38:10.897 GCDDemo[44461:636252] 1———并发队列异步执行<NSThread: 0x60800007a700>{number = 4, name = (null)}
2017-05-03 17:38:10.899 GCDDemo[44461:636254] 1———并发队列异步执行<NSThread: 0x60800007ac40>{number = 5, name = (null)}
2017-05-03 17:38:10.900 GCDDemo[44461:636251] 1———并发队列异步执行<NSThread: 0x60800007a800>{number = 3, name = (null)}
2017-05-03 17:38:10.903 GCDDemo[44461:636252] 2———并发队列异步执行<NSThread: 0x60800007a700>{number = 4, name = (null)}
2017-05-03 17:38:10.904 GCDDemo[44461:636254] 2———并发队列异步执行<NSThread: 0x60800007ac40>{number = 5, name = (null)}
2017-05-03 17:38:10.905 GCDDemo[44461:636251] 2———并发队列异步执行<NSThread: 0x60800007a800>{number = 3, name = (null)}
总结: 并发队列异步执行会开辟多线程执行,并且执行顺序不定
各种组合的结果总结如下表
同步执行 | 异步执行 | |
---|---|---|
主线程队列 | 出现死锁 | 不会开辟多线程,在主线程上串行执行 |
串行队列 | 不会开辟多线程,在一条线程上串行执行 | 不会开辟多线程,在一条线程上串行执行 |
并发队列(全局队列) | 不会开辟多线程,在一条线程上串行执行 | 开辟多线程执行,并且执行顺序不定 |
划重点:
- 并发队列只会在异步执行下才会开启多线程执行
- 在主线程队列同步执行,或者串行队列嵌套串行队列同步任务会发生死锁
- dispatch_async: 不会阻塞当前队列,立即返回添加当前线程后面任务。
- dispatch_sync:会阻塞当前队列,等该sync任务全部执行完毕之后再添加当前队列后面任务
应用示例
异步处理数据完成后,主线程更新UI界面
1
2
3
4
5
6dispatch\_async(globalQueue, ^{
//异步数据处理...
dispatch_async(mainQueue, ^{
//主线程更新UI
});
});
可以看到运用GCD可以轻松的进行线程间通信
使用指定队列进行串行处理任务,例如数据库存储等依赖线程安全等处理
1
2
3
4//串行队列保证线程安全
dispatch_sync(serialQueue, ^{
//数据存储等依赖线程安全操作...
});
FMDBDataBaseQueue就是使用的串行队列来保证线程安全的
其他GCD API
dispatch_after
1 | dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC); |
延迟执行,有时候可以作为定时执行作用,需要注意的是,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue
,实际执行时间受到runloop的状态影响,存在偏差。
dispatch_once
1 | static dispatch_once_t onceToken; |
执行一次,在block中的代码全局只会执行一次,被广泛用于单例创建中
dispatch_suspend / dispatch_resume
1 | //挂起队列 |
挂起对已经执行的任务没有影响,会暂停所有未执行的任务以及后续追加的任务
恢复则会继续执行所有被挂起的任务
dispatch_set_target_queue
1 | //搬运一段代码 |
dispatch_set_target_queue主要有两个作用
- 设置优先级,自建的队列优先级默认和系统队列优先级一致,设置参数1队列的优先级和参数2的优先级一致,显然你不能设置系统全局队列和主队列优先级
- 更改队列的执行层级,如果多个串行队列设置函数目标串行队列是某一个串行队列,原本并发执行的串行队列,在目标串行队列上只能依次执行,代码示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"1 in");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"1 out");
});
dispatch_async(queue2, ^{
NSLog(@"2 in");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"2 out");
});
dispatch_async(queue3, ^{
NSLog(@"3 in");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"3 out");
});
dispatch_group
主要应对这样的需求,异步处理完A和B任务,两者都执行完执行C任务,和NSOperation中的依赖一致。示例如下
1 | dispatch_group_t group = dispatch_group_create(); |
dispatch_apply
这是dispatch_sync
和dispatch_group
的关联API,按指定次数将指定的Block追加到指定的Dispatch_Queue
中,并且等待全部执行结束。可以用于遍历效果
1 | //全局队列 |
dispatch_barrier_sync / dispatch_barrier_async
栅栏:有时候创建两组并发任务,如果在中间加入栅栏,那么这个任务会在第一组任务完成后执行,并且第二组任务会在栅栏任务完成后才开始执行,如下图所示在并发队列中添加任务,执行顺序一定是
任务组A->Barrier任务->任务组B
示例代码如下:
1 | dispatch_queue_t concurrentQueue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); |
打印执行顺序1,2不定,3,4也不定,但是barrier一定在1和2之后,3和4一定在barrier之后,可以自行添加数量测试。
应用场景,经常我们会自行创建一个队列进行文件读取和存储,一般文件读取的速度很快,可以使用并发队列多线程提高读取效率,但是文件存储需要考虑到线程安全,那么我们就可以使用barrier进行文件存储操作,类似这样
1 | dispatch_queue_t concurrentQueue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); |
可见使用barrier可以轻松高效的实现文件IO。
dispatch_barrier
需要注意的点
dispatch_barrier
只会对自建的队列生效,对于系统的mainQueue和GlobalQueue不起作用dispatch_barrier_async
和dispatch_barrier_sync
的区别也同样在于同步和异步,dispatch_barrier_async
不会等待自己任务执行完毕才会在队列中添加其他任务,而dispatch_barrier_sync
会等待自己任务执行完毕后才会在队列中添加其他任务。
AFNetworking中大量使用dispatch_barrier_async
做数据存储,可以看到dispatch_barrier_async
也可以实现串行同步队列效果,相比于dispatch_sync
容易产生死锁(在串行队列中同步添加该串行队列任务即会发生死锁),dispatch_barrier_async
更加安全。
全文总结:
以上介绍了GCD中绝大多数常用API,可以看到GCD的灵活性,通过在应用中合理使用GCD来提高程序的执行效率。