本文来自:https://www.jianshu.com/p/33151a5bac28
介绍
GCD,英文全称是Grand Central Dispatch(功能强悍的中央调度器),基于C语言编写的一套多线程开发机制,因此使用时会以函数形式出现,且大部分函数以dispatch开头,虽然是C语言的但对于苹果其他多线程实现方式,抽象层次更高,使用起来也更加方便。
它是苹果为应对多核的并行运算提出的解决方案,它会自动利用多核并发处理和运算,它能提供系统级别的处理,而不再局限于某个进程、线程,官方声明会更快、更高效、更灵敏,且线程由系统自动管理(调度、运行),无需程序员参与,使用起来非常方便。
任务和队列
GCD有2个核心:任务和队列
- 任务:要执行的操作或方法函数,
- 队列:存放任务的集合,而我们要做的就是将任务添加到队列然后执行,GCD会自动将队列中的任务按先进先出的方式提取并交付给对应的线程。注意任务的取出是按照先进先出的方式,这也是队列的特性,但是取出后执行的顺序不一定,下面会详细讨论。
1任务
任务是一个比较抽象的概念,可以简单的认为是一个操作、一个函数、一个方法等等。在实际的开发中大多是以block的形式,使用起来更加灵活。
2队列queue
- 有2种队列:串行队列和并行队列。串行队列:同步执行,在当前线程执行;并行队列:可由多个线程异步执行,但任务取出还是FIFO的。
队列创建,根据函数的第二个参数来创建串行或并行队列
1 | // 参数1 队列名称 |
- 另外系统还提供了两种队列:全局队列和主队列
全局队列属于并行队列,只不过由系统创建没有名字,且在全局可见(可用)。获取全局队列
1 | /* 取得全局队列 |
主队列属于串行队列,也由系统创建,只不过运行在主线程(UI线程)。获取主队列
1 | serialQ = dispatch_get_main_queue(); |
关于内存:queue属于一个对象,也是占用内存的,也会使用引用计数,当queue添加一个任务时就会将这个queue retain一下,引用计数+1,直到所有任务都完成内存才会释放。(我们在声明queue属性时要用strong)。
3执行方式–2种
同步执行和异步执行
同步执行:不会开启新的线程,在当前线程执行。
异步执行:GCD管理的线程池中有空闲线程就会从队列中取出任务执行,会开启线程。
下面为实现同步和异步的函数,函数功能为:将任务添加到队列并执行
1 | /* 同步执行 |
注意:默认情况下,新线程都没有开启runloop,所有当block任务完成后,线程都会自动被回收,假设我们想在新开的线程中使用NSTimer,就必须启用runloop。可以使用[[NSRunLoop currentRunLoop] run] 开启当前线程,这时就需要自己管理线程的回收等工作。
- 另外还用2个方法,实际开发中用的并不是太多
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
和dispatch_barrier_async(dispatch_queue_t queue, disaptch_block_t block);
加了一个barrier,意义在于:队列之前的block处理完成之后才开始处理队列中barrier的block,且barrier的block必须处理完成之后,才能处理其他的block。
三、几种类型
很明显两种执行方式,两种队列。那么就有4种情况:串行队列同步执行、
串行队列异步执行、并行队列同步执行、并行队列异步执行。哪一种会开启新的线程?开几条?是否并发?记忆起来比较绕,但是只要抓住基本的就可以,为了方便理解,现分析如下:
- 串行队列,同步执行——串行队列意味着顺序执行,同步执行意味着不开启线程(在当前线程执行)
- 串行队列,异步执行——串行队列意味着任务顺序执行,异步执行说明要开线程。(如果开多个线程的话,不能保证串行队列顺序执行,所以只能开一个线程)
- 并行队列,异步执行——并行队列意味着执行顺序不确定,异步执行意味着会开启线程,而并行队列又不允许不按顺序执行,所以系统为了提高性能会开启多个线程,来队列提取任务(队列中的任务取出仍然要顺序取出的,只是线程执行无序)。
- 并行队列,同步执行——同步执行意味着不开线程,责肯定是顺序执行的。
- 死锁—-程序执行不出来
四、死锁举例
主队列死锁:
这种死锁最常见,问题也最严重,会造成主线程卡住.原因:主队列,如果主线程正在执行代码,就不调度任务;同步执行:一直执行第一个任务直到结束.两者相互等待造成死锁,示例代码如下.
1 | - (void)mainThreadDeadLockTest{ |
其他线程死锁,这种不会影响主线程:
原因: serialQueue 为串行队列,当代码执行到block1时正常,执行到dispatch_sync时.dispatch_sync等待block2执行完才能返回,而serialQueue是串行队列,它正在执行block1,只有等block1执行完毕之后才会去执行block2,相互等待造成死锁.
1 | - (void)deadLockTest { |
五、常用举例
- 线程间通讯
比如,为了提高用户体验,我们一般在其他线程(非主线程)下载图片或其它网络资源,下载完成后我们要更新UI,而UI更新必须在主线程执行,所以我们经常会使用
1 | // 同步执行,不阻塞直到下面block中的代码执行完毕 |
信号量的使用
也属于线程间通讯,下面的举例是经常使用到的场景,在网络访问中,NSURLSession类都是异步的(找了好久没有找到同步的方法),而有时我们希望能够像NSURLConnection一样可以同步访问,即在网络block调用完之后做一些操作.那我们可以使用dispatch的信号量来解决.
1 | ///用于线程间通讯,下面是等待一个网络完成 |
在上面的举例中, dispatch_semaphore_signal 的调用必须是在另一个线程调用,因为当前线程已经 dispatch_semaphore_wait阻塞.另外,dispatch_semaphore_wait最好不要在主线程调用
全局队列,实现并发:
1
2
3dispatch_async(dispatch_get_global_queue(0,0), ^{
//要执行相关代码
})
六、Dispatch Group
使用调度组,可以轻松实现一下任务完成后,做一些操作.比如具有顺序性要求的生产者消费者等等.
1 | - (void)groupTest { |
点击屏幕后打印如下,可以看到任务1虽然等待了1s,任务2也不执行,只有任务1执行完成之后才去执行任务2.
示例2:其实示例1并不常用,真正用到的是监控多个任务完成之后,回到主线程更新UI,或者做其它事情
1 | -(void)groupTest1 { |
点击屏幕打印如下,无论其他任务的执行完成顺序,mainTask等待他们执行完成后才执行.
019-03-30 21:15:13.903164+0800 GDC[11358:631441] 开始执行
2019-03-30 21:15:13.903705+0800 GDC[11358:631487] task 1 running in <NSThread: 0x600000b6d240>{number = 4, name = (null)}
2019-03-30 21:15:13.903754+0800 GDC[11358:631488] task2 runing in <NSThread: 0x600000b60340>{number = 3, name = (null)}
2019-03-30 21:15:14.906108+0800 GDC[11358:631490] task4 runing in <NSThread: 0x600000b6d340>{number = 5, name = (null)}
2019-03-30 21:15:15.903843+0800 GDC[11358:631489] task3 runing in <NSThread: 0x600000b6d480>{number = 6, name = (null)}
2019-03-30 21:15:15.904085+0800 GDC[11358:631441] mainTask running in<NSThread: 0x600000b3e900>{number = 1, name = main}
关于GCD的内存管理问题
根据上面的代码,可以看出有关dispatch的对象并不是OC对象,那么,用不用像对待Core Foundation框架的对象一样,使用retain、release来管理它呢?答案是不用的.
如果是ARC环境,我们无需管理,会像对待OC对象一样自动内存管理.如果是MRC环境,不是使用retain、release,而是使用dispath_retain/dispatch_release 来管理.