-
① 线程、进程与队列
①.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
取代,以下是相关的枚举值
5 iOS中多线程的实现方案
iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation
,汇总如图所示
6 线程安全问题
当多个线程同时访问一块资源时,容易引发数据错乱和数据安全问题,有以下两种解决方案
- 互斥锁(即同步锁):
@synchronized
- 自旋锁
6.1 互斥锁 vs 自旋锁
互斥锁
- 保证锁内的代码,同一时间,只有一条线程能够执行!
- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
- 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠
- 能够加锁的任意
NSObject
对象 - 注意:锁对象一定要保证所有的线程都能够访问
- 如果代码中只有一个地方需要加锁,大多都使用
self
,这样可以避免单独再创建一个锁对象
自旋锁
- 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于
忙等
(即原地打转,称为自旋)阻塞状态 - 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符
atomic
,本身就有一把自旋锁
- 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用
死循环
的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能
考考你: 自旋锁vs互斥锁的区别?
-
相同点:在同一时间,保证了只有一条线程执行任务,即保证了相应同步的功能
-
不同点:
互斥锁
:发现其他线程执行,当前线程休眠
(即就绪状态
),进入等待执行,即挂起.一直等其他线程打开之后,然后唤醒执行自旋锁
:发现其他线程执行,当前线程忙等
(即一直访问),处于忙等状态,耗费的性能比较高
-
使用场景:根据任务复杂度区分,使用不同的锁
-
当前的任务状态比较
短小精悍
时,用自旋锁
-
反之的,用
互斥锁
-
6.2 atomic与nonatomic 的区别
atomic
和 nonatomic
主要用于属性的修饰,以下是相关的一些说明
nonatomic
非原子属性atomic
原子属性(线程安全),针对多线程设计的,默认值- 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
atomic
本身就有一把锁(自旋锁
)- 单写多读:单个线程写入,多个线程可以读取
atomic
:线程安全,需要消耗大量的资源nonatomic
:非线程安全,适合内存小的移动设备
iOS
开发的建议
-
所有属性都声明为
nonatomic
-
尽量避免多线程抢夺同一块资源 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
六、相关试题解析
① 异步函数+并行队列
下面代码的输出顺序是什么?
异步函数并不会阻塞主队列,会开辟新线程执行异步任务
分析思路如下图所示,红线表示任务的执行顺序
主线程
的任务队列为:任务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
7 线程间通讯
-
直接消息传递
: 通过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
是苹果官方提供面向对象的线程操作技术,是对thread
的上层封装,比较偏向于底层.简单方便,可以直接操作线程对象,使用频率较少.
① 创建线程
线程的创建方式主要有以下三种方式
-
通过
init
初始化方式创建 -
通过
detachNewThreadSelector
构造器方式创建 -
通过
performSelector...
方法创建,主要是用于获取主线程
,以及后台线程
NSthread
是苹果官方提供面向对象的线程操作技术,是对thread
的上层封装,比较偏向于底层.简单方便,可以直接操作线程对象,使用频率较少.
类方法
常用的类方法有以下几个
currentThread
:获取当前线程sleep...
:阻塞线程exit
:退出线程mainThread
:获取主线程
① GCD简介
什么是GCD
? GCD
全称是Grand Central Dispatch
,它是纯 C
语言,并且提供了非常多强大的函数
GCD
的优势:
GCD
是苹果公司为多核的并行运算
提出的解决方案GCD
会自动利用
更多的CPU内核
(比如双核、四核)GCD
会自动管理
线程的生命周期(创建线程、调度任务、销毁线程)- 程序员只需要告诉
GCD
想要执行什么任务,不需要编写任何线程管理代码
用一句话总结GCD就是:将任务添加到队列,并且指定执行任务的函数
在日常开发中,GCD
一般写成下面这种形式
将上述代码拆分,方便我们来理解GCD
的核心,主要是由 任务 + 队列 + 函数
构成
- 使用
dispatch_block_t
创建任务 - 使用
dispatch_queue_t
创建队列 - 将任务添加到队列,并指定执行任务的函数
dispatch_async
注意 这里的任务
是指执行操作
的意思,在使用dispatch_block_t
创建任务时,主要有以下两点说明
-
任务使用
block
封装 -
任务的
block
没有参数也没有返回值
在GCD
中执行任务的方式有两种,同步执行
和异步执行
,分别对应同步函数dispatch_sync
和 异步函数dispatch_async
,两者对比如下
同步执行
,对应同步函数dispatch_sync
- 必须等待当前语句执行完毕,才会执行下一条语句
不会开启线程
,即不具备开启新线程的能力- 在当前线程中执行
block
任务
异步执行
,对应异步函数dispatch_async
- 不用等待当前语句执行完毕,就可以执行下一条语句
会开启线程
执行block
任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关)- 异步是多线程的代名词
两种执行方式的主要区别
有两点:
-
是否等待
队列的任务执行完毕 -
是否具备开启新线程
的能力
队列
多线程中所说的队列
(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底层分析
由于源码的篇幅较大、逻辑分支、宏定义较多,使得源码变得晦涩难懂,让开发者们望而却步.但如果带着疑问、有目的性的去看源码,就能减少难度,忽略无关的代码.首先提出我们要分析的几个问题:
-
队列创建
-
异步函数
-
同步函数
-
单例的原理
-
栅栏函数的原理
-
信号量的原理
-
调度组的原理
作者:長茳
链接:juejin.cn/post/693719…
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。