RunLoop
和线程的关系,以及Thread
如何保活和控制生命周期,今天我们再探究下另外的一个线程GCD
,揭开蒙娜丽莎的面纱。
GCD 基础知识
GCD是什么呢?我们引用百度百科的一段话。
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。
GCD有哪些优点
- GCD自动管理线程
- 开发者只需要将task加入到队列中,不用关注细节,然后将task执行完的block传入即可
- GCD 自动管理线程,线程创建,挂起,销毁。
那么我们研究下如何更好的使用GCD,首先要了解到串行队列、并行队列、并发
串行队列
串行是基于队列的,队列会自己控制线程,在串行队列中,任务一次只能执行一个,执行完当前任务才能继续执行下个任务。
并行队列
并行有通过新建线程来实现并发执行任务,并行队列中同时是可能执行多个任务,当并行数量没有限制的时候,理论上所有任务可以同时执行。
并发
并发是基于线程的,同一个线程只能串行(同一时刻)执行,要想实现并发,只能多个线程一起干活
串行队列相当于工厂1条流水线4个工人生产设备,从开始到结束,一个人只能干一件事,甲做A不做B。
并行队列是一条流水线4个工人,当工人干活速度不够的时候可以再申请一条流水线,实现两条流水线同时干活,这就实现了并发。
并发是多个流水线在同时加工产品。
GCD中的串行队列()
串行队列(Serial Dispatch Queue):
按照FIFO(First In First Out先进先出)原则,先添加的任务在队首,后添加的任务在队尾,执行任务的时候按照队列的从首到尾一个挨着一个执行,一次只能执行一个任务,不具备开辟新线程的能力。
并发队列(Concurrent Dispatch Queue):
按照FIFO(First In First Out先进先出)原则,先添加的任务在队首,后添加的任务在队尾,执行任务的时候按照队列的从首到若干个,执行到队尾,一次可以执行多个任务,具备开辟新线程的能力。
GCD使用步骤
GCD的使用非常简单,创建队列或者在全局队列中新加任务就可以了。
下边来看看 队列的创建方法/获取方法,以及 任务的创建方法。
获取主队列
主队列是一种特殊的队列,也是串行队列,负责UI的更新,也可以做其他事情,可以通过dispatch_get_main_queue()
,一般写的代码没有声明多线程或者添加到其他队列中的代码都是在主队列中运行的。
1 | //获取主队列 |
获取全局队列
全局队列是一个特殊的并行队列,系统已经创建好了,使用的时候通过dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
,第一个参数是identifier
,表示队列的优先级,一般传入DISPATCH_QUEUE_PRIORITY_DEFAULT
,第二个参数flags
,官方说法是必须是0,否则返回NULL。暂且传入0。下边摘自libdispatch
Use the
.Fn dispatch_get_global_queue
function to obtain the global queue of given priority. The
.Fa flags
argument is reserved for future use and must be zero. Passing any value other
than zero may result in a NULL return value.
1 | //获取全局队列 |
任务的创建
GCD 提供了同步执行任务的创建方法dispatch_sync
和异步执行任务创建方法的spatch_async
。
1 | // 同步执行任务创建方法 |
虽然是只有同步异步但是他们组合的多变的
并发队列 | 创建的串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没开启新线程,串行执行 | 没开启新线程,串行执行任务 | 没开启新线程,串行执行任务 |
异步(async) | 能开启新线程,并发执行 | 能开启新线程,串行执行任务 | 没开启新线程,串行执行任务 |
GCD的使用
主队列+同步
在主队列中执行任务,并同步添加任务
1 | //主队列+同步 |
看到日志只输出了1就崩溃了提示exc_bad_instuction
,为什么出问题呢?
主队列是同步的,任务前后执行的任务是在主队列中,添加的任务也是在主队列中,而且添加是同步添加。
what???在同步队列中添加同步任务,到底是想让队列执行任务还是添加任务。队列遵循FIFO原则,假如要大家都在排队等打饭,新来的员工叫的A,后边代码叫B,然后都在一个队列中,突然来了个插队的,你说B能同意吗?明显和A干起来了,结果系统老师过来拉架了说了一句exc_bad_instuction
,意思是你俩吵起来大家都吃不上饭了,结果他俩还是接着吵,把系统吵崩溃了。
那么我们能在主队列中同步添加任务吗?答案是可以的。看到答案不要笑哦
1 | //主队列+同步 |
没看错,保证在主队列中调用该函数,那么他就是主队列同步执行的,如果在其他队列中调用,那它则是在调用者队列中同步执行。
主队列+异步
在主队列中异步添加任务并执行任务
1 | //主队列+异步 |
在主队列异步执行任务,从日志看出来end
早于任务的执行,符合FIFO原则,都是在主线程执行,可以看到
- 主线程多个任务异步不能创建新线程
- 主线程异步也是串行执行
全局队列+同步
全局队列是并行队列,和同步配合就是串行执行了。
1 | //全局队列+同步 |
在全局队列中使用串行添加多个任务并没有新建子线程来解决问题,同步其实就是串行,使用FIFO原则,一个任务解决完再解决下一个任务。
全局队列+异步
全局队列有创建子线程的能力,但是需要异步async
去执行。
1 | //全局队列+异步 |
全局队列当搭配async
的时候,追加多个任务,这次是使用3个线程,而且不用我们来维护线程的生命周期,而且执行的顺序是无序的。
创建串行队列+同步
开发者自己创建的串行队列同步调用和系统主队列有类似的地方,也有区别。一样都是串行执行,区别是追加任务的时候一般是在主队列向串行队列添加。
1 | //创建串行队列+同步 |
同步向串行队列添加任务并没有死锁!原因是添加任务是在main_queue
执行的,添加的任务是在cust-queue
中执行,符合FIFO原则,先添加的先执行,具体执行的线程由他们自己分配。执行的任务是在main
线程中。
创建串行队列+异步
会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
1 | //创建串行队列+异步 |
在异步 + 串行队列
可以看到:
开启了一条新线程(异步执行具备开启新线程的能力,串行队列只开启一个线程)。
所有任务是在打印的end
之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。
创建并行队列+同步
在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务
1 | start |
全局队列其实就是特殊的并行队列,这里结果和全局队列+同步
一致。
创建并行队列+异步
在当前线程中执行任务,会开启新线程,可以同时执行多个任务。
1 | //创建并行队列+异步 |
并行队列+异步
和全局队列+异步
一致,也会新建线程执行任务,且是并发执行。
GCD其他高级用法
子线程执行任务 主线程刷新UI
1 | - (void)backToMain{ |
队列分组 dispatch_group_t
dispatch_group_notify
GCD有有分组的概念,当所有加入分组的队列中的任务都执行完成的时候,通过dispatch_group_notify
完成回调,第一个参数group
是某个分组的回调。
1 | -(void)group{ |
dispatch_group_wait && dispatch_group_enter && dispatch_group_leave
dispatch_group_enter
和dispatch_group_leave
需要成对使用,否则dispatch_group_wait
在缺少leave
的情况下会等待到死,造成线程阻塞。
1 | static dispatch_group_t group ; |
栅栏函数 dispatch_barrier_sync
栅栏函数实现了异步的队列中在多个任务结束的时候实行回调,回调分异步和同步,同步回调在主线程,异步在其他线程。
1 | - (void)barry{ |
单例-执行一次的函数 dispatch_once_t
单例可以通过这个函数实现,只执行一次的函数。
1 | //只执行一次的dispatch_once |
当调用4次的时候,日志打印的四次obj
均为同一个地址,证明block
回调四次但是只执行了一次。
延迟执行 dispatch_after
当记录日志或者点击事件的方法我们不希望立即执行,则会用到延迟
1 | //延迟执行 |
信号量 dispatch_semaphore_t
信号量为1可以作为线程锁来用,当N>1的时候,同时执行的有N个任务。dispatch_apply
可以通知创建多个线程来执行任务,用它来测试信号量再好不过了。
1 | //信号量 当信号量为1 可以未做锁来用,当N>1,t通知执行的数量则是数字N。 |
设计一个经典问题,火车票窗口买票,火车站卖票一般有多个窗口,排队是每个窗口排一个队列,一个窗口同时只能卖一张票,那我们设计一下如何实现多队列同时访问多个窗口的的问题。
1 | -(void)muchQueueBuyTick{ |
两个窗口(两个队列),每个窗口排了5(循环5次)个人,一共10(count=10)张票。
当同时一张票可以分割2次,卖票的错乱了,明显错误了,现在把每张票都锁起来,同时只能允许同一个人卖。
1 | -(void)muchQueueBuyTick{ |
顺序是对了,数量也对了。
再换一种思路实现锁住窗口,我们使用串行队列也是可以的。
1 | //使用同步队列卖票 |
串行队列不创建子线程,所有任务都在同一个线程执行,那么他们就会排队,其实不管多少人同时点击买票,票的分割还是串行的,所以线程锁的可以使用串行队列来解决。
快速迭代方法:dispatch_apply
快速迭代就是同时创建很多线程来在做事情,现在工厂收到一个亿的订单,工厂本来只有2条生产线,现在紧急新建很多生产线来生产产品。
1 | /* |
可以看到新建了3
、4
、5
、6
、7
、8
、9
、main
来执行任务。
多线程RunLoop实战
问题一:请问下边代码输出什么?
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ |
- 猜想1:结果是
123
- 猜想2:结果是
132
有没有第三种结果呢?
猜想1分析:
因为是延迟0
s执行,当然是先执行2
,再执行3
了。
猜想2分析:
我们来分析一下,异步加入全局队列中,单个任务的时候会加入到子线程中,那么会先输出1
,然后输出3
,最后输出2
.
最后验证一下:
1 | 1 |
为什么2没有出来呢?在看一下代码,全局队列,延迟执行,点进去函数查看,原来是在runloop.h
文件中,我们猜测延迟执行是timer
添加到runloop
中了,添加进去也应该输出132
的。因为在子线程中,没有主动调用不会有runloop
的,及时调用了也需要保活技术,那么代码改进一下
1 | dispatch_queue_t que= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
经测试输出了12
,这和我们猜想的还是不对,原来输出3
放在了最后,导致的问题,RunLoop
运行起来,进入了循环,则后面的就不会执行了,除非停止当前RunLoop
,我们再改进一下
1 | dispatch_queue_t que= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
最后终于输出了132
。缺点是子线程成了死待,不死之身,关于怎么杀死死待请看上篇优雅控制RunLoop生命周期。
关于performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
中有延迟的,都是添加到当前你线程的RunLoop
,如果没有启动RunLoop
和保活恐怕也不能一直执行。[self performSelector:@selector(test) withObject:nil]
是在Foudation
中,源码是直接objc_msgSend()
,相当于直接[self test]
,不会有延迟。
问题2:请问输出什么?
1 | NSThread *thread=[[NSThread alloc]initWithBlock:^{ |
这个和上面的类似,结果是打印了1
就崩溃了,原因是thread start
之后执行完block
就结束了,没有runloop
的支撑。当执行performSelector
的时候,线程已经死掉。解决这个问题只需要向子线程中添加RunLoop
,而且保证RunLoop
不停止就行了。
总结
- GCD异步负责执行耗时任务(例如下载,复杂计算),main线程负责更新UI
- 队列多任务异步执行最后全局执行完毕可以使用
group_notify
来监听执行完毕时间 - 队列多任务异步执行结束时间,中间拦截更新UI,然后再异步执行可以使用
dispatch_barrier_sync
- 当多线程访问同一个资源,可以使用信号量来限制同时访问资源的线程数量
参考资料
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间