写在前面
多线程在iOS中有着举足轻重的地位,那么本篇文章就来带你全面走进她.....
一、基本概念及原理
① 线程、进程与队列
①.1 线程的定义
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行- 进程想要执行任务,必须得有线程,
进程至少要有一条线程 -
程序启动会默认开启一条线程,这条线程被成为主线程或UI线程
①.2 进程的定义
-
进程是指在系统中正在运行的一个应用程序,如微信、支付宝app都是一个进程 - 每个
进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内 - 通过“活动监视器”可以查看mac系统中所开启的线程
所以,可以简单的理解为:进程是线程的容器,而线程用来执行任务.在iOS中是单进程开发,一个进程就是一个app,进程之间是相互独立的,如支付宝、微信、qq等,这些都是属于不同的进程.
①.3 进程与线程的关系和区别
- 地址空间:同一
进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间 - 资源拥有:同一
进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的 - 两个之间的关系就相当于
工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即进程相当于一个工厂,线程相当于工厂中的一条流水线 - 一个
进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮 -
进程切换时,消耗的资源大、效率高.所以设计到频繁的切换时,使用线程要好于进程.同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程而不能用进程 - 执行过程:每个独立的
进程有一个程序运行的入口、顺序执行序列和程序入口.但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 -
线程是处理器调度的基本单位,但进程不是 - 线程没有地址空间,线程包含在进程地址空间中
可能会觉得这些理论知识很抽象,百度出来一大堆但是都不好理解,看完下面的理解就全明白了
①.4 进程与线程的关系图
可以把iOS系统想象成商场,进程则是商场中的店铺,线程是店铺雇佣的员工:
- 进程之间的相互独立
- 奶茶店看不到果汁店的账目(访问不了别的进程的内存)
- 果汁店用不了奶茶店的波霸(进程之间的资源是独立的)
- 进程至少要有一条线程
- 店铺至少要有一个员工(进程至少有一个线程)
- 早上开店门的员工(相当于主线程)
- 进程/线程崩溃的情况
- 奶茶店倒闭了并不会牵连果汁店倒闭(进程崩溃不会对其他进程产生影响)
- 奶茶店的收银员不干了会导致奶茶店无法正常运作(线程崩溃导致进程瘫痪)
移动开发不一定是单进程处理的,android就是多进程处理的;而iOS采用沙盒机制,这也是苹果运行能够流畅安全的一个主要原因
①.5 线程和runloop的关系
-
runloop与线程是一一对应的—— 一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里 -
runloop是来管理线程的—— 当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务 -
runloop在第一次获取时被创建,在线程结束时被销毁- 对于主线程来说,
runloop在程序一启动就默认创建好了 - 对于子线程来说,
runloop是懒加载的 —— 只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调
- 对于主线程来说,
①.6 影响任务执行速度的因素
以下因素都会对任务的执行速度造成影响:
-
cpu的调度 - 线程的执行速率
- 队列情况
- 任务执行的复杂度
- 任务的优先级
② 多线程
②.1 多线程原理
- 对于
单核CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作(执行) -
iOS中的多线程同时执行的本质是CPU在多个任务之间进行快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果.其中切换的时间间隔就是时间片
②.2 多线程意义
优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU、内存)
- 线程上的任务执行完成后,线程会自动销毁
缺点
- 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占
512KB,创建线程大约需要90毫秒的创建时间) - 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,
CPU在调用线程上的开销就越大 - 程序设计更加复杂,比如线程间的通信、多线程的数据共享
②.3 多线程生命周期
多线程的生命周期主要分为5部分:新建 - 就绪 - 运行 - 阻塞 - 死亡,如下图所示
-
新建:主要是实例化线程对象 -
就绪:线程对象调用start方法,将线程对象加入可调度线程池,等待CPU的调用,即调用start方法,并不会立即执行,进入就绪状态,需要等待一段时间,经CPU调度后才执行,也就是从就绪状态进入运行状态 -
运行:CPU负责调度可调度线程池中线程的执行.在线程执行完成之前,其状态可能会在就绪和运行之间来回切换.就绪和运行之间的状态变化由CPU负责,程序员不能干预. -
阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行.sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁) -
死亡:分为两种情况:正常死亡,即线程执行完毕. 非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片)
- 如果
时间片用尽,线程就会进入就绪状态队列 - 如果
时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列 - 等待事件发生后,线程又会重新进入
就绪状态队列 - 每当一个
线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列中选择一个线程继续执行
线程的exit和cancel说明
-
exit:一旦强行终止线程,后续的所有代码都不会执行 -
cancel:取消当前线程,但是不能取消正在执行的线程
那么是不是线程的优先级越高,意味着任务的执行越快?
并不是,线程执行的快慢,除了要看优先级,还需要查看资源的大小(即任务的复杂度)、以及 CPU 调度情况.在NSThread中,线程优先级threadPriority已经被服务质量qualityOfService取代,以下是相关的枚举值
②.4 线程池的原理
-
【第一步】判断核心线程池是否都正在执行任务
- 返回NO,创建新的工作线程去执行
- 返回YES,进入【第二步】
-
【第二步】判断线程池工作队列是否已经饱满
- 返回NO,将任务存储到工作队列,等待CPU调度
- 返回YES,进入【第三步】
-
【第三步】判断线程池中的线程是否都处于执行状态
- 返回NO,安排可调度线程池中空闲的线程去执行任务
- 返回YES,进入【第四步】
-
【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)
-
AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行 -
CallerRunsPolicy:将任务回退到调用者 -
DisOldestPolicy:丢掉等待最久的任务 -
DisCardPolicy:直接丢弃任务
-
②.5 iOS中多线程的实现方案
iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation,汇总如图所示
下面是以上四种方案的简单示例
C和OC的桥接
其中涉及C与OC的桥接,有以下几点说明
-
__bridge只做类型转换,但是不修改对象(内存)管理权 -
__bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用CFRelease或者相关方法来释放对象 -
__bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC
②.6 线程安全问题
当多个线程同时访问一块资源时,容易引发数据错乱和数据安全问题,有以下两种解决方案
- 互斥锁(即同步锁):
@synchronized - 自旋锁
②.6.1 互斥锁 vs 自旋锁
互斥锁
- 保证锁内的代码,同一时间,只有一条线程能够执行!
- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
- 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠
- 能够加锁的任意
NSObject对象 - 注意:锁对象一定要保证所有的线程都能够访问
- 如果代码中只有一个地方需要加锁,大多都使用
self,这样可以避免单独再创建一个锁对象
自旋锁
- 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于
忙等(即原地打转,称为自旋)阻塞状态 - 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符
atomic,本身就有一把自旋锁 - 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用
死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能
考考你: 自旋锁vs互斥锁的区别?
-
相同点:在同一时间,保证了只有一条线程执行任务,即保证了相应同步的功能
-
不同点:
-
互斥锁:发现其他线程执行,当前线程休眠(即就绪状态),进入等待执行,即挂起.一直等其他线程打开之后,然后唤醒执行 -
自旋锁:发现其他线程执行,当前线程忙等(即一直访问),处于忙等状态,耗费的性能比较高
-
-
使用场景:根据任务复杂度区分,使用不同的锁
- 当前的任务状态比较
短小精悍时,用自旋锁 - 反之的,用
互斥锁
- 当前的任务状态比较
②.6.2 atomic与nonatomic 的区别
atomic 和 nonatomic主要用于属性的修饰,以下是相关的一些说明
-
nonatomic非原子属性 -
atomic原子属性(线程安全),针对多线程设计的,默认值- 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
-
atomic本身就有一把锁(自旋锁) - 单写多读:单个线程写入,多个线程可以读取
-
atomic:线程安全,需要消耗大量的资源 -
nonatomic:非线程安全,适合内存小的移动设备
iOS 开发的建议
- 所有属性都声明为
nonatomic - 尽量避免多线程抢夺同一块资源 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
②.7 线程间通讯
在Threading Programming Guide文档中,提及,线程间的通讯有以下几种方式
[图片上传失败...(image-a8fb76-1614613062037)]
-
直接消息传递: 通过performSelector的一系列方法,可以实现由某一线程指定在另外的线程上执行任务.因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化 -
全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块.尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱.必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性. 否则可能会导致竞争状况,数据损坏或崩溃。 -
条件执行: 条件是一种同步工具,可用于控制线程何时执行代码的特定部分.您可以将条件视为关守,让线程仅在满足指定条件时运行. -
Runloop sources: 一个自定义的Runloop source配置可以让一个线程上收到特定的应用程序消息.由于Runloop source是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率 -
Ports and sockets:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术.更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信.为了提高效率,使用Runloop source来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态.需要注意的是,端口通讯需要将端口加入到主线程的Runloop中,否则不会走到端口回调方法 -
消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据.尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效 -
Cocoa 分布式对象: 分布式对象是一种Cocoa技术,可提供基于端口的通信的高级实.尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销.分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高.
②.8 GCD和NSOperation的比较
-
GCD和NSOperation的关系如下:-
GCD是面向底层的C语言的API -
NSOperation是用GCD封装构建的,是GCD的高级抽象
-
-
GCD和NSOperation的对比如下:
-
GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构 —— 写起来更加方便 -
GCD只支持FIFO的队列,而NSOpration可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序 -
NSOpration甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier任务才能控制执行顺序,较为复杂 -
NSOperation支持KVO(面向对象)可以检测operation是否正在执行、是否结束、是否取消
-
二、NSthread
NSthread是苹果官方提供面向对象的线程操作技术,是对thread的上层封装,比较偏向于底层.简单方便,可以直接操作线程对象,使用频率较少.
① 创建线程
线程的创建方式主要有以下三种方式
- 通过
init初始化方式创建 - 通过
detachNewThreadSelector构造器方式创建 - 通过
performSelector...方法创建,主要是用于获取主线程,以及后台线程
② 属性
③ 类方法
常用的类方法有以下几个
-
currentThread:获取当前线程 -
sleep...:阻塞线程 -
exit:退出线程 -
mainThread:获取主线程
三、GCD
① GCD简介
什么是GCD?
GCD全称是Grand Central Dispatch,它是纯 C 语言,并且提供了非常多强大的函数
GCD的优势:
-
GCD是苹果公司为多核的并行运算提出的解决方案 -
GCD会自动利用更多的CPU内核(比如双核、四核) -
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程) - 程序员只需要告诉
GCD想要执行什么任务,不需要编写任何线程管理代码
用一句话总结GCD就是:将任务添加到队列,并且指定执行任务的函数
② GCD核心
在日常开发中,GCD一般写成下面这种形式
将上述代码拆分,方便我们来理解GCD的核心,主要是由 任务 + 队列 + 函数 构成
- 使用
dispatch_block_t创建任务 - 使用
dispatch_queue_t创建队列 - 将任务添加到队列,并指定执行任务的函数
dispatch_async
注意
这里的任务是指执行操作的意思,在使用dispatch_block_t创建任务时,主要有以下两点说明
- 任务使用
block封装 - 任务的
block没有参数也没有返回值
③ 函数与队列
③.1 函数
在GCD中执行任务的方式有两种,同步执行和异步执行,分别对应同步函数dispatch_sync 和 异步函数dispatch_async,两者对比如下
-
同步执行,对应同步函数dispatch_sync- 必须等待当前语句执行完毕,才会执行下一条语句
-
不会开启线程,即不具备开启新线程的能力 - 在当前线程中执行
block任务
-
异步执行,对应异步函数dispatch_async- 不用等待当前语句执行完毕,就可以执行下一条语句
-
会开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关) - 异步是多线程的代名词
综上所述,两种执行方式的主要区别有两点:
-
是否等待队列的任务执行完毕 -
是否具备开启新线程的能力
③.2 队列
多线程中所说的队列(Dispatch Queue)是指执行任务的等待队列,即用来存放任务的队列.队列是一种特殊的线性表,遵循先进先出(FIFO)原则,即新任务总是被插入到队尾,而任务的读取从队首开始读取.每读取一个任务,则动队列中释放一个任务,如下图所示
③.2.1 串行队列 和 并发队列
在GCD中,队列主要分为串行队列(Serial Dispatch Queue) 和并发队列(Concurrent Dispatch Queue)两种,如下图所示
-
串行队列:每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个,即只开启一个线程(通俗理解:同一时刻只调度一个任务执行)- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);创建串行队列 - 其中的
DISPATCH_QUEUE_SERIAL也可以使用NULL表示,这两种均表示默认的串行队列
- 使用
-
并发队列:一次可以并发执行多个任务,即开启多个线程,并同时执行任务(通俗理解:同一时刻可以调度多个任务执行)- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);创建并发队列 - 注意:并发队列的并发功能只有在
异步函数下才有效
- 使用
③.2.2 主队列 和 全局并发队列
在GCD中,针对上述两种队列,分别提供了主队列(Main Dispatch Queue)和全局并发队列(Global Dispatch Queue)
-
主队列(Main Dispatch Queue):GCD中提供的特殊的串行队列- 专门用来
在主线程上调度任务的串行队列,依赖于主线程、主Runloop,在main函数调用之前自动创建 - 不会开启线程
- 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度
- 使用
dispatch_get_main_queue()获得主队列 - 通常在返回
主线程更新UI时使用
- 专门用来
-
全局并发队列(Global Dispatch Queue):GCD提供的默认的并发队列 - 为了方便程序员的使用,苹果提供了全局队列
- 在使用多线程开发时,如果对队列没有特殊需求,在执行
异步任务时,可以直接使用全局队列 - 使用
dispatch_get_global_queue获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)- 第一个参数表示
队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已经被服务质量(quality-of-service)取代 -
第二个参数使用0
- 第一个参数表示
③.2.3 全局并发队列 + 主队列 配合使用
在日常开发中,全局队列+并发并列一般是这样配合使用的
③.3 函数与队列的不同组合
主队列和全局队列单独考虑,组合结果以总结表格为准
③.3.1 串行队列 + 同步函数
任务一个接一个的在当前线程执行,不会开辟新线程
③.3.2 串行队列 + 异步函数
任务一个接一个的执行,会开辟新线程
③.3.3 并发队列 + 同步函数
任务一个接一个的执行,不开辟线程
③.3.4 并发队列 + 异步函数
任务乱序执行,会开辟新线程
③.3.5 主队列 + 同步函数
任务相互等待,造成死锁
造成死锁的原因分析如下:
- 主队列有两个任务,顺序为:
CJNSLog任务-同步block - 执行
CJNSLog任务后,执行同步Block,会将任务1(即i=1时)加入到主队列,主队列顺序为:CJNSLog任务 - 同步block - 任务1 -
任务1的执行需要等待同步block执行完毕才会执行,而同步block的执行需要等待任务1执行完毕,所以就造成了任务互相等待的情况,即造成死锁崩溃
死锁现象
-
主线程因为你同步函数的原因等着先执行任务 -
主队列等着主线程的任务执行完毕再执行自己的任务 -
主队列和主线程相互等待会造成死锁
③.3.6 主队列 + 异步函数
任务一个接一个的执行,不开辟线程
③.3.7 全局并发队列 + 同步函数
任务一个接一个的执行,不开辟新线程
③.3.8 全局并发队列 + 异步函数
任务乱序执行,会开辟新线程
③.3.9 总结
| 函数与队列 | 串行队列 | 并发队列 | 主队列 | 全局并发队列 |
|---|---|---|---|---|
| 同步函数 | 顺序执行,不开辟线程 | 顺序执行,不开辟线程 | 死锁 | 顺序执行,不开辟线程 |
| 异步函数 | 顺序执行,开辟线程 | 乱序执行,开辟线程 | 顺序执行,不开辟线程 | 乱序执行,开辟线程 |
④ dispatch_after
⑤ dispatch_once
⑥ dispatch_apply
⑦ dispatch_group_t
dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间
应用场景:多个接口请求之后刷新页面
有以下两种使用方式
⑦.1 使用dispatch_group_async + dispatch_group_notify
dispatch_group_notify在dispatch_group_async执行结束之后会受收到通知
⑦.2 使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
dispatch_group_enter和dispatch_group_leave成对出现,使进出组的逻辑更加清晰
调度组要注意搭配使用,必须先进组再出组,缺一不可
⑦.3 在⑦.2 的基础上使用 dispatch_group_wait
⑧ dispatch_barrier_sync & dispatch_barrier_async
栅栏函数,主要有两种使用场景:串行队列、并发队列.
应用场景:同步锁
等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中.
简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务.
⑧.1 串行队列使用栅栏函数
不使用栅栏函数
使用栅栏函数
栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2
结论:由于串行队列异步执行任务是一个接一个执行完毕的,所以使用栅栏函数没意义
⑧.2 并发队列使用栅栏函数
不使用栅栏函数
使用栅栏函数
结论:由于并发队列异步执行任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序
⑧.3 dispatch_barrier_sync/dispatch_barrier_async区别
-
dispatch_barrier_async:前面的任务执行完毕才会来到这里 -
dispatch_barrier_sync:作用相同,但是这个会堵塞线程,影响后面的任务执行
将案例二中的dispatch_barrier_async改成dispatch_barrier_sync
结论:dispatch_barrier_async可以控制队列中任务的执行顺序,而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)
⑧.4 栅栏函数注意点
- 1.
尽量使用自定义的并发队列:- 使用
全局队列起不到栅栏函数的作用 - 使用
全局队列时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)
- 使用
- 2.
栅栏函数只能控制同一并发队列:打个比方,平时在使用AFNetworking做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列
⑨ dispatch_semaphore_t
信号量主要用作同步锁,用于控制GCD最大并发数
-
dispatch_semaphore_create():创建信号量 -
dispatch_semaphore_wait():等待信号量,信号量减1.当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到信号(signal)才执行下去 -
dispatch_semaphore_signal():释放信号量,信号量加1.当信号量>= 0会执行wait之后的代码.
下面这段代码要求使用信号量来按序输出(当然栅栏函数可以满足要求)
利用信号量的API来进行代码改写
如果当创建信号量时传入值为1又会怎么样呢?
-
i=0时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为0不会阻塞线程,所以进入i=1 -
i=1时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为-1阻塞线程,等待signal再执行下去
结论:
- 创建信号量时传入值为1时,可以通过两次才堵塞
- 传入值为2时,可以通过三次才堵塞
⑩ dispatch_source
dispatch_source_t主要用于计时操作,其原因是因为它创建的timer不依赖于RunLoop,且计时精准度比NSTimer高
⑩.1 定义及使用
dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件
-
Timer Dispatch Source:定时器事件源,用来生成周期性的通知或回调 -
Signal Dispatch Source:监听信号事件源,当有UNIX信号发生时会通知 -
Descriptor Dispatch Source:监听文件或socket事件源,当文件或socket数据发生变化时会通知 -
Process Dispatch Source:监听进程事件源,与进程相关的事件通知 -
Mach port Dispatch Source:监听Mach端口事件源 -
Custom Dispatch Source:监听自定义事件源
主要使用的API:
-
dispatch_source_create: 创建事件源 -
dispatch_source_set_event_handler: 设置数据源回调 -
dispatch_source_merge_data: 设置事件源数据 -
dispatch_source_get_data: 获取事件源数据 -
dispatch_resume: 继续 -
dispatch_suspend: 挂起 -
dispatch_cancle: 取消
⑩.2 自定义定时器
在iOS开发中一般使用NSTimer来处理定时逻辑,但NSTimer是依赖Runloop的,而Runloop可以运行在不同的模式下.如果NSTimer添加在一种模式下,当Runloop运行在其他模式下的时候,定时器就挂机了;又如果Runloop在阻塞状态,NSTimer触发时间就会推迟到下一个Runloop周.。因此NSTimer在计时上会有误差,并不是特别精确,而GCD定时器不依赖Runloop,计时精度要高很多
使用dispatch_source自定义定时器注意点:
-
GCDTimer需要强持有,否则出了作用域立即释放,也就没有了事件回调 -
GCDTimer默认是挂起状态,需要手动激活 -
GCDTimer没有repeat,需要封装来增加标志位控制 -
GCDTimer如果存在循环引用,使用weak+strong或者提前调用dispatch_source_cancel取消timer -
dispatch_resume和dispatch_suspend调用次数需要平衡 -
source在挂起状态下,如果直接设置source = nil或者重新创建source都会造成crash.正确的方式是在激活状态下调用dispatch_source_cancel(source)释放当前的source
四、NSOperation
NSOperation是个抽象类,依赖于子类NSInvocationOperation、NSBlockOperation去实现
下面是开发者文档上对NSOperation的一段描述
① NSInvocationOperation
-
基本使用
-
直接处理事务,不添加隐性队列
-
接下来就会引申出下面一段错误使用代码
上述代码之所以会崩溃,是因为线程生命周期:
-
queue addOperation:op已经将处理事务的操作任务加入到队列中,并让线程运行 -
op start将已经运行的线程再次运行会造成线程混乱
② NSBlockOperation
NSInvocationOperation和NSBlockOperation两者的区别在于:
- 前者类似
target形式 - 后者类似
block形式——函数式编程,业务逻辑代码可读性更高
NSOperationQueue是异步执行的,所以任务一、任务二的完成顺序不确定
通过addExecutionBlock这个方法可以让NSBlockOperation实现多线程
③ 自定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务
④ NSOperationQueue
NSOperationQueue有两种队列:主队列、其他队列.其他队列包含了 串行和并发.
- 主队列:主队列上的任务是在主线程执行的
- 其他队列(非主队列):加入到
非主队列中的任务默认就是并发,开启多线程
例如我们在② NSBlockOperation中说的那样.
⑤ 执行顺序
下列代码可以证明操作与队列的执行效果是异步并发的
⑥ 设置优先级
NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成
-
不使用
sleep——高优先级的任务一先于低优先级的任务二
-
使用
sleep进行延时——高优先级的任务一慢于低优先级的任务二
⑦ 设置并发数
- 在
GCD中只能使用信号量来设置并发数 - 而
NSOperation轻易就能设置并发数- 通过设置
maxConcurrentOperationCount来控制单次出队列去执行的任务数
- 通过设置
⑧ 添加依赖
在NSOperation中添加依赖能很好的控制任务执行的先后顺序
⑨ 线程间通讯
- 在
GCD中使用异步进行网络请求,然后回到主线程刷新UI -
NSOperation中也有类似在线程间通讯的操作
⑩ 任务的挂起、继续、取消
但是在使用中经常会遇到一些匪夷所思的问题——明明已经挂起了任务,可还是继续执行了几个任务才停止执行
这幅图是并发量为2的情况:
- 挂起前:
任务3、任务4等待被调度 - 挂起瞬间:
任务3、任务4已经被调度出队列,准备执行,此时它们是无法挂起的 - 挂起后:
任务3、任务4被线程执行,而原来的队列被挂起不能被调度
五、GCD底层分析
由于源码的篇幅较大、逻辑分支、宏定义较多,使得源码变得晦涩难懂,让开发者们望而却步.但如果带着疑问、有目的性的去看源码,就能减少难度,忽略无关的代码.首先提出我们要分析的几个问题:
- 队列创建
- 异步函数
- 同步函数
- 单例的原理
- 栅栏函数的原理
- 信号量的原理
- 调度组的原理
① 源码的出处
分析源码首先得获取到GCD源码,之前已经分析过objc、malloc、dyld源码,那么GCD内容是在哪份源码中呢?
这里分享一个小技巧,由于已知要研究GCD,所以有以下几种选择源码的方法
- Baidu/Google
- 下符号断点
dispatch_queue_create或dispatch_async,打开汇编调式Debug->Debug Workflow->Always show Disassembly
这样子就找到了我们需要的libdispatch-1271.40.12源码
② 队列创建
通过前面的学习我们知道队列的创建是通过GCD中的dispatch_queue_create方法创建的,因此可以在源码中搜索dispatch_queue_create.
假如我们就直接搜索dispatch_queue_create的话,会出现众多的情况(66 results in 18 files),这时候就考验一个开发者阅读源码的经验了
在此,我们就要改一改搜索条件了:
- 由于创建队列代码为
dispatch_queue_create("XXX", NULL),所以搜索dispatch_queue_create(—— 将筛选结果降至(21 results in 6 files)
- 由于第一个参数为字符串,在
c语言中用const修饰,所以搜索dispatch_queue_create(const—— 将筛选结果降至(2 results in 2 files)
②.1 dispatch_queue_create
常规中间层封装 —— 便于代码迭代不改变上层使用
有时候也需要注意下源码中函数中的传参:
- 此时
label是上层的逆序全程域名,主要用在崩溃调试 -
attr是NULL/DISPATCH_QUEUE_SERIAL、DISPATCH_QUEUE_CONCURRENT,用于区分队列是异步还是同步的
#define DISPATCH_QUEUE_SERIAL NULL串行队列的宏定义其实是个NULL
②.2 _dispatch_lane_create_with_target
-
1.通过
_dispatch_queue_attr_to_info方法传入dqa(即队列类型,串行、并发等)创建dispatch_queue_attr_info_t类型的对象dqai,用于存储队列的相关属性信息
-
dispatch_queue_attr_info_t与isa一样,是个位域结构,用于存储队列的相关属性信息
-
- 2.设置队列相关联的属性,例如服务质量qos等
- 3.通过
DISPATCH_VTABLE拼接队列名称,即vtable,其中DISPATCH_VTABLE是宏定义,如下所示,所以队列的类型是通过OS_dispatch_+队列类型queue_concurrent拼接而成的- 串行队列类型:
OS_dispatch_queue_serial,验证如下
- 串行队列类型:
* 并发队列类型:`OS_dispatch_queue_concurrent`,验证如下
- 4.通过
alloc+init初始化队列,即dq,其中在_dispatch_queue_init传参中根据dqai.dqai_concurrent的布尔值,就能判断队列是串行还是并发,而vtable表示队列的类型,说明队列也是对象- 进入
_dispatch_object_alloc -> _os_object_alloc_realized方法中设置了isa的指向,从这里可以验证队列也是对象的说法
- 进入
* 进入`_dispatch_queue_init`方法,队列类型是`dispatch_queue_t`,并设置队列的相关属性
- 5.通过
_dispatch_trace_queue_create对创建的队列进行处理,其中_dispatch_trace_queue_create是_dispatch_introspection_queue_create封装的宏定义,最后会返回处理过的_dq
* 进入`_dispatch_introspection_queue_create_hook -> dispatch_introspection_queue_get_info -> _dispatch_introspection_lane_get_info`中可以看出,与我们自定义的类还是有所区别的,`创建队列`在底层的实现是`通过模板创建`的
②.3 总结
- 队列创建方法
dispatch_queue_create中的参数二(即队列类型),决定了下层中max & 1(用于区分是 串行 还是 并发),其中1表示串行 -
queue也是一个对象,也需要底层通过alloc + init创建,并且在alloc中也有一个class,这个class是通过宏定义拼接而成,并且同时会指定isa的指向 -
创建队列在底层的处理是通过模板创建的,其类型是dispatch_introspection_queue_s结构体
dispatch_queue_create底层分析流程如下图所示
③ 异步函数
③.1 dispatch_async
主要分析两个函数
-
_dispatch_continuation_init:任务包装函数 -
_dispatch_continuation_async:并发处理函数
③.2 _dispatch_continuation_init 任务包装器
主要是包装任务,并设置线程的回程函数,相当于初始化
主要有以下几步
- 通过
_dispatch_Block_copy拷贝任务 - 通过
_dispatch_Block_invoke封装任务,其中_dispatch_Block_invoke是个宏定义,根据以上分析得知是异步回调
- 如果是
同步的,则回调函数赋值为_dispatch_call_block_and_release - 通过
_dispatch_continuation_init_f方法将回调函数赋值,即f就是func,将其保存在属性中
③.3 _dispatch_continuation_async 并发处理
这个函数中,主要是执行block回调
- 其中的关键代码是
dx_push(dqu._dq, dc, qos),dx_push是宏定义,如下所示
- 而其中的
dq_push需要根据队列的类型,执行不同的函数
在此我们通过符号断点调试执行函数
- 运行
demo,通过符号断点,来判断执行的是哪个函数,由于是并发队列,通过增加_dispatch_lane_concurrent_push符号断点,看看是否会走到这里
- 运行发现,走的确实是
_dispatch_lane_concurrent_push
- 进入
_dispatch_lane_concurrent_push源码,发现有两步,继续通过符号断点_dispatch_continuation_redirect_push和_dispatch_lane_push调试,发现走的是_dispatch_continuation_redirect_push
- 进入
_dispatch_continuation_redirect_push源码,发现又走到了dx_push,即递归了,综合前面队列创建时可知,队列也是一个对象,有父类、根类,所以会递归执行到根类的方法
- 接下来,通过根类的
_dispatch_root_queue_push符号断点,来验证猜想是否正确,从运行结果看出,完全是正确的
- 进入
_dispatch_root_queue_push -> _dispatch_root_queue_push_inline ->_dispatch_root_queue_poke -> _dispatch_root_queue_poke_slow源码,经过符号断点验证,确实是走的这里,查看该方法的源码实现,主要有两步操作- 通过
_dispatch_root_queues_init方法注册回调 - 通过
do-while循环创建线程,使用pthread_create方法
- 通过
③.4 _dispatch_root_queues_init
- 进入
_dispatch_root_queues_init源码实现,发现是一个dispatch_once_f单例(请查看后续单例的底层分析们,这里不作说明),其中传入的func是_dispatch_root_queues_init_once
- 进入
_dispatch_root_queues_init_once的源码,其内部不同事务的调用句柄都是_dispatch_worker_thread2
其block回调执行的调用路径为:_dispatch_root_queues_init_once ->_dispatch_worker_thread2 -> _dispatch_root_queue_drain -> _dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline -> _dispatch_client_callout -> dispatch_call_block_and_release
这个路径可以通过断点,bt打印堆栈信息得出
在这里需要说明一点的是,单例的block回调和异步函数的block回调是不同的
- 单例中,
block回调中的func是_dispatch_Block_invoke(block) - 而异步函数中,
block回调中的func是dispatch_call_block_and_release
④ 总结
综上所述,异步函数的底层分析如下
-
准备工作: 首先,将异步任务拷贝并封装,并设置回调函数func -
block回调:底层通过dx_push递归,会重定向到根队列,然后通过pthread_creat创建线程,最后通过dx_invoke执行block回调(注意dx_push和dx_invoke是成对的)
异步函数的底层分析流程如图所示
④ 同步函数
④.1 dispatch_sync
其底层的实现是通过栅栏函数实现的(栅栏函数的底层分析见后文)
④.2 _dispatch_sync_f
④.3 _dispatch_sync_f_inline
查看_dispatch_sync_f_inline源码,其中width = 1表示是串行队列,其中有两个重点:
- 栅栏:
_dispatch_barrier_sync_f(可以通过后文的栅栏函数底层分析解释),可以得出同步函数的底层实现其实是同步栅栏函数 - 死锁:
_dispatch_sync_f_slow,如果存在相互等待的情况,就会造成死锁
④.4 _dispatch_sync_f_slow 死锁
进入_dispatch_sync_f_slow,当前的主队列是挂起、阻塞的
- 往一个队列中加入任务,会
push加入主队列,进入_dispatch_trace_item_push
- 进入
__DISPATCH_WAIT_FOR_QUEUE__,判断dq是否为正在等待的队列,然后给出一个状态state,然后将dq的状态和当前任务依赖的队列进行匹配
- 进入
_dq_state_drain_locked_by -> _dispatch_lock_is_locked_by源码
如果当前等待的和正在执行的是同一个队列,即判断线程ID是否相等,如果相等,则会造成死锁
同步函数 + 并发队列 顺序执行的原因
在_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline源码中,主要有三个步骤:
- 将任务压入队列:
_dispatch_thread_frame_push - 执行任务的
block回调:_dispatch_client_callout - 将任务出队:
_dispatch_thread_frame_pop
从实现中可以看出,是先将任务push队列中,然后执行block回调,在将任务pop,所以任务是顺序执行的
④.5 总结
同步函数的底层实现如下:
-
同步函数的底层实现实际是同步栅栏函数 - 同步函数中如果
当前正在执行的队列和等待的是同一个队列,形成相互等待的局面,则会造成死锁
⑤ dispatch_once 单例
在日常开发中,我们一般使用GCD的dispatch_once来创建单例,如下所示
首先对于单例,我们需要了解两点
-
执行一次的原因: 单例的流程只执行一次,底层是如何控制的,即为什么只能执行一次? -
block调用时机: 单例的block是在什么时候进行调用的?
下面带着以上两点疑问,我们来针对单例的底层进行分析
⑤.1 dispatch_once
进入dispatch_once源码实现,底层是通过dispatch_once_f实现的
- 参数1:
onceToken,它是一个静态变量,由于不同位置定义的静态变量是不同的,所以静态变量具有唯一性 - 参数2:
block回调
⑤.2 dispatch_once_f
进入dispatch_once_f源码,其中的val是外界传入的onceToken静态变量,而func是_dispatch_Block_invoke(block),其中单例的底层主要分为以下几步
- 将
val,也就是静态变量转换为dispatch_once_gate_t类型的变量l - 通过
os_atomic_load获取此时的任务的标识符v- 如果
v等于DLOCK_ONCE_DONE,表示任务已经执行过了,直接return - 如果 任务执行后,
加锁失败了,则走到_dispatch_once_mark_done_if_quiesced函数,再次进行存储,将标识符置为DLOCK_ONCE_DONE - 反之,则通过
_dispatch_once_gate_tryenter尝试进入任务,即解锁,然后执行_dispatch_once_callout执行block回调
- 如果
- 如果此时有任务正在执行,再次进来一个任务2,则通过
_dispatch_once_wait函数让任务2进入无限次等待
⑤.3 _dispatch_once_gate_tryenter 解锁
查看其源码,主要是通过底层os_atomic_cmpxchg方法进行对比,如果比较没有问题,则进行加锁,即任务的标识符置为DLOCK_ONCE_UNLOCKED
⑤.4 _dispatch_once_callout 回调
进入_dispatch_once_callout源码,主要就两步
-
_dispatch_client_callout:block回调执行 -
_dispatch_once_gate_broadcast:进行广播
- 进入
_dispatch_client_callout源码,主要就是执行block回调,其中的f等于_dispatch_Block_invoke(block),即异步回调
- 进入
_dispatch_once_gate_broadcast -> _dispatch_once_mark_done源码,主要就是给dgo->dgo_once一个值,然后将任务的标识符为DLOCK_ONCE_DONE,即解锁
⑤.5 总结
针对单例的底层实现,主要说明如下:
-
单例只执行一次的原理:GCD单例中,有两个重要参数,onceToken和block,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量l,l主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return -
block调用时机:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义.加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回 -
多线程影响:如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的
单例的底层流程分析如下如所示
⑥ 栅栏函数
GCD中常用的栅栏函数,主要有两种
-
同步栅栏函数dispatch_barrier_sync(在主线程中执行):前面的任务执行完毕才会来到这里,但是同步栅栏函数会堵塞线程,影响后面的任务执行 -
异步栅栏函数dispatch_barrier_async:前面的任务执行完毕才会来到这里
栅栏函数最直接的作用就是 控制任务执行顺序,使同步执行
栅栏函数需要注意以下几点
- 栅栏函数只能控制
同一并发队列 -
同步栅栏添加进入队列的时候,当前线程会被锁死,直到同步栅栏之前的任务和同步栅栏任务本身执行完毕时,当前线程才会打开然后继续执行下一句代码 - 在使用栅栏函数时,使用
自定义队列才有意义- 如果栅栏函数中使用
全局队列,运行会崩溃,原因是系统也在用全局并发队列,使用栅栏同时会拦截系统的,所以会崩溃 - 如果将自定义并发队列改为串行队列,即serial ,串行队列本身就是有序同步 此时加栅栏,会浪费性能
- 如果栅栏函数中使用
⑥.1 异步栅栏函数
进入dispatch_barrier_async源码实现,其底层的实现与dispatch_async类似,这里就不再做分析了,有兴趣的可以自行探索下
⑥.2 同步栅栏函数
⑥.2.1 dispatch_barrier_sync
进入dispatch_barrier_sync源码,实现如下
⑥.2.2 _dispatch_barrier_sync_f_inline
进入_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline源码
主要有分为以下几部分
- 通过
_dispatch_tid_self获取线程ID - 通过
_dispatch_queue_try_acquire_barrier_sync判断线程状态
* 进入`_dispatch_queue_try_acquire_barrier_sync_and_suspend`,在这里进行释放
- 通过
_dispatch_sync_recurse递归查找栅栏函数的target - 通过
_dispatch_introspection_sync_begin对向前信息进行处理
- 通过
_dispatch_lane_barrier_sync_invoke_and_complete执行block并释放
⑦ 信号量
信号量的作用一般是用来使任务同步执行,类似于互斥锁,用户可以根据需要控制GCD最大并发数.前面我们已经说了怎么使用了
下面我们来分析其底层原理
⑦.1 dispatch_semaphore_create 创建
该函数的底层实现如下,主要是初始化信号量,并设置GCD的最大并发数,其最大并发数必须大于0
⑦.2 dispatch_semaphore_wait 加锁
该函数的源码实现如下,其主要作用是对信号量dsema通过os_atomic_dec2o进行了--操作,其内部是执行的C++的atomic_fetch_sub_explicit方法
- 如果
value >= 0,表示操作无效,即执行成功 - 如果
value = LONG_MIN,系统会抛出一个crash - 如果
value < 0,则进入长等待
其中os_atomic_dec2o的宏定义转换如下
os_atomic_inc2o(p, f, m)
os_atomic_sub2o(p, f, 1, m)
_os_atomic_c11_op((p), (v), m, sub, -)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
memory_order_##m); (__typeof__(_r))(_r op _v); })
将具体的值代入为
os_atomic_dec2o(dsema, dsema_value, acquire);
os_atomic_sub2o(dsema, dsema_value, 1, m)
os_atomic_sub(dsema->dsema_value, 1, m)
_os_atomic_c11_op(dsema->dsema_value, 1, m, sub, -)
_r = atomic_fetch_sub_explicit(dsema->dsema_value, 1),
等价于 dsema->dsema_value - 1
进入_dispatch_semaphore_wait_slow的源码实现,当value < 0时,根据等待事件timeout做出不同操作
⑦.3 dispatch_semaphore_signal 解锁
该函数的源码实现如下,其核心也是通过os_atomic_inc2o函数对value进行了++操作,os_atomic_inc2o内部是通过C++的atomic_fetch_add_explicit
- 如果
value > 0,表示操作无效,即执行成功 - 如果
value < 0,则进入长等待
其中os_atomic_dec2o的宏定义转换如下
os_atomic_inc2o(p, f, m)
os_atomic_add2o(p, f, 1, m)
os_atomic_add(&(p)->f, (v), m)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
memory_order_##m); (__typeof__(_r))(_r op _v); })
将具体的值代入为
os_atomic_inc2o(dsema, dsema_value, release);
os_atomic_add2o(dsema, dsema_value, 1, m)
os_atomic_add(&(dsema)->dsema_value, (1), m)
_os_atomic_c11_op((dsema->dsema_value), (1), m, add, +)
_r = atomic_fetch_add_explicit(dsema->dsema_value, 1),
等价于 dsema->dsema_value + 1
⑦.4 总结
-
dispatch_semaphore_create主要就是初始化限号量 -
dispatch_semaphore_wait是对信号量的value进行--,即加锁操作 -
dispatch_semaphore_signal是对信号量的value进行++,即解锁操作
⑧ 调度组的原理
调度组的最直接作用是控制任务执行顺序,常见方式如下
⑧.1 dispatch_group_create
- 进入
dispatch_group_create源码
主要是创建group,并设置属性,此时的group的value为0
- 进入
_dispatch_group_create_with_count源码,其中是对group对象属性赋值,并返回group对象,其中的n等于0
⑧.2 dispatch_group_enter 进组
进入dispatch_group_enter源码,通过os_atomic_sub_orig2o对dg->dg.bits 作 --操作,对数值进行处理
⑧.3 dispatch_group_leave 出组
进入dispatch_group_leave源码,可知
- -1 到 0,即
++操作 - 根据状态,
do-while循环,唤醒执行block任务 - 如果
0 + 1 = 1,enter-leave不平衡,即leave多次调用,会crash
- 进入
_dispatch_group_wake源码,do-while循环进行异步命中,调用_dispatch_continuation_async执行
- 进入
_dispatch_continuation_async源码
这步与异步函数的block回调执行是一致的,这里不再作说明
⑧.4 dispatch_group_notify 通知
进入dispatch_group_notify源码,如果old_state等于0,就可以进行释放了
除了leave可以通过_dispatch_group_wake唤醒,其中dispatch_group_notify也是可以唤醒的
- 其中
os_mpsc_push_update_tail是宏定义,用于获取dg的状态码
⑧.5 dispatch_group_async
进入dispatch_group_async 源码,主要是包装任务和异步处理任务
- 进入
_dispatch_continuation_group_async源码,主要是封装了dispatch_group_enter进组操作
-
进入
_dispatch_continuation_async源码,执行常规的异步函数底层操作.既然有了enter,肯定有leave,我们猜测block执行之后隐性的执行leave,通过断点调试,打印堆栈信息
-
搜索
_dispatch_client_callout的调用,在_dispatch_continuation_with_group_invoke中
所以,完美的印证dispatch_group_async底层封装的是enter-leave
⑧.6 总结
-
enter-leave只要成对就可以,不管远近 -
dispatch_group_enter在底层是通过C++函数,对group的value进行--操作(即0 -> -1) -
dispatch_group_leave在底层是通过C++函数,对group的value进行++操作(即-1 -> 0) -
dispatch_group_notify在底层主要是判断group的state是否等于0,当等于0时,就通知 -
block任务的唤醒,可以通过dispatch_group_leave,也可以通过dispatch_group_notify -
dispatch_group_async等同于enter - leave,其底层的实现就是enter-leave
六、相关试题解析
① 异步函数+并行队列
下面代码的输出顺序是什么?
异步函数并不会阻塞主队列,会开辟新线程执行异步任务
分析思路如下图所示,红线表示任务的执行顺序
-
主线程的任务队列为:任务1、异步block1、任务5,其中异步block1会比较耗费性能,任务1和任务5的任务复杂度是相同的,所以任务1和任务5优先于异步block1执行 - 在
异步block1中,任务队列为:任务2、异步block2、任务4,其中block2相对比较耗费性能,任务2和任务4是复杂度一样,所以任务2和任务4优先于block2执行 - 最后执行
block2中的任务3 - 在极端情况下,可能出现
任务2先于任务1和任务5执行,原因是出现了当前主线程卡顿或者延迟的情况
扩展一
将并行队列 改成 串行队列,对结果没有任何影响,顺序仍然是1 5 2 4 3
扩展二
在任务5之前,休眠2s,即sleep(2),执行的顺序为:1 2 4 3 5,原因是因为I/O的打印,相比于休眠2s,复杂度更简单,所以异步block1 会先于任务5执行.当然如果主队列堵塞,会出现其他的执行顺序
② 异步函数嵌套同步函数 + 并发队列
下面代码的输出顺序是什么?
分析如下:
-
任务1和任务5的分析同前面一致,执行顺序为任务1 任务5 异步block - 在
异步block中,首先执行任务2,然后走到同步block,由于同步函数会阻塞主线程,所以任务4需要等待任务3执行完成后,才能执行,所以异步block中的执行顺序是:任务2 任务3 任务4
③ 异步函数嵌套同步函数 + 串行队列(即同步队列)
下面代码的执行顺序是什么?会出现什么情况?为什么?
分析如下图所示,红色表示任务执行顺序,黑色虚线表示等待
- 首先执行
任务1,接下来是异步block,并不会阻塞主线程,相比任务5而言,复杂度更高,所以优先执行任务5,在执行异步block - 在
异步block中,先执行任务2,接下来是同步block,同步函数会阻塞线程,所以执行任务4需要等待任务3执行完成,而任务3的执行,需要等待异步block执行完成,相当于任务3等待任务4完成 - 所以就造成了
任务4等待任务3,任务3等待任务4,即互相等待的局面,就会造成死锁,这里有个重点是关键的堆栈slow
扩展一
去掉任务4,执行顺序是什么?
还是会死锁,因为任务3等待的是异步block执行完毕,而异步block等待任务3.
④ 异步函数 + 同步函数 + 并发队列
下面代码的执行顺序是什么?(答案是 AC)
A: 1230789
B: 1237890
C: 3120798
D: 2137890
分析
-
任务1和任务2由于是异步函数+并发队列,会开启线程,所以没有固定顺序 -
任务7、任务8、任务9同理,会开启线程,所以没有固定顺序 -
任务3是同步函数+并发队列,同步函数会阻塞主线程,但是也只会阻塞0,所以,可以确定的是0一定在3之后,在789之前
以下是不同的执行顺序的打印
⑤ 下面代码中,队列的类型有几种?
队列总共有两种: 并发队列 和 串行队列
- 串行队列:
serial、mainQueue - 并发队列:
conque、globalQueue
写在后面
和谐学习,不急不躁.我还是我,颜色不一样的烟火.










网友评论