写在前面
多线程在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`,验证如下![](https://upload-images.jianshu.io/upload_images/2340353-ac444e89cd7608be.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 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`,并设置队列的相关属性![](https://upload-images.jianshu.io/upload_images/2340353-bc9fbe87c7aea2e9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 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`中可以看出,与我们自定义的类还是有所区别的,`创建队列`在底层的实现是`通过模板创建`的![](https://upload-images.jianshu.io/upload_images/2340353-8558202b2f11d069.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
②.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`,在这里进行释放![](https://upload-images.jianshu.io/upload_images/2340353-4ccaf0f24ed3b69a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 通过
_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
写在后面
和谐学习,不急不躁.我还是我,颜色不一样的烟火.