多线程相关概念
同步、异步
同步:完成需要做的任务后才会返回,进行下一任务。不会开新的线程,会在当前线程中执行任务。
异步:不会等待任务完成才返回,会立即返回。必定会开启新的线程,线程的申请是由异步负责,起到开分支的作用。
串、并行、全局、主队列
串行队列: 任务依次执行,同一时间队列中只有一个任务在执行,每个任务只有在前一个任务执行完成后才能开始执行。
并行队列:任务并发执行,你唯一能保证的是,这些任务会按照被添加的顺序开始执行。但是任务可以以任何顺序完成。
全局队列:隶属于并行队列
主队列:隶属于串行队列,不能与 sync 同步方法搭配使用,会造成死循环
交叉关系
— | 同步 | 异步 |
---|---|---|
串行队列 | 不会新建线程,依然在当前线程上类似同步锁,是同步锁的替代方案✅ 常用 | 会新建线程,只开一条线程一条线程就够了 每次使用 createDispatch 方法就会新建一条线程,多次调用该方法,会创建多条线程,多条线程间会并行执行 |
并行队列 | 不会新建线程,依然在当前线程上 | 会新建线程,可以开多条线程 iOS7-SDK 时代一般是5、6条, iOS8-SDK 以后可以50、60条 ✅ 常用 |
iOS中的多线程技术
iOS中的三种多线程技术:
- NSThread
- NSOperation/NSOperationQueue
- GCD(Grand Central Dispatch)
NSThead
NSThead 启动流程
1 | start()->创建pthread->执行main -> [target performSelector:selector] -> exit; |
NSOperation/NSOperationQueue
NSOperation
、NSOperationQueue
是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
使用NSOperation
、NSOperationQueue
有一下几种优势:
- 可添加完成的代码块,在操作完成后执行。
- 添加操作之间的依赖关系,方便的控制执行顺序。
- 设定操作执行的优先级。
- 可以很方便的取消一个操作的执行。
- 可以控制最大并发量
- 使用 KVO 观察对操作执行状态的更改:isReady、isExecuteing、isFinished、isCancelled。
NSOperation
表示了一个独立的计算单元。作为一个抽象类,它给了它的子类一个十分有用而且线程安全的方式来建立状态、优先级、依赖性和取消等的模型。或者,你不是很喜欢再自己继承NSOperation
的话,框架还提供NSInvocationOperation
、NSBlockOperation
两个继承自NSOperation
的类。
所以我们有三种方式来封装操作:
- 使用子类 NSInvocationOperation
- 使用子类 NSBlockOperation
- 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。
但是仅仅把计算封装进一个对象而不做其他处理显然没有多大用处,我们还需要NSOperationQueue
来大显身手。NSOperationQueue
控制着这些并行操作的执行,它扮演者优先级队列的角色,让它管理的高优先级操作(NSOperation -queuePriority
)能优先于低优先级的操作运行的情况下,使它管理的操作能基本遵循先进先出的原则执行。此外,在你设置了能并行运行的操作的最大值(maxConcurrentOperationCount
)之后,NSOperationQueue
还能并行执行操作。
让一个NSOperation
操作开始,你可以直接调用-start
,或者将它添加到NSOperationQueue
中,添加之后,它会在队列排到它以后自动执行。
现在让我们通过怎样使用和怎样通过继承实现功能来看看NSOperation
稍微复杂的部分。
状态
NSOperation
包含了一个十分优雅的状态机来描述每一个操作的执行。
1 | isReady → isExecuting → isFinished |
为了替代不那么清晰的state属性,状态直接由上面那些keypath的KVO通知决定,也就是说,当一个操作在准备好被执行的时候,它发送了一个KVO通知给isReady
的keypath,让这个keypath对应的属性isReady
在被访问的时候返回YES。
每一个属性对于其他的属性必须是互相独立不同的,也就是同时只可能有一个属性返回YES,从而才能维护一个连续的状态:- isReady
: 返回 YES 表示操作已经准备好被执行, 如果返回NO则说明还有其他没有先前的相关步骤没有完成。 - isExecuting
: 返回YES表示操作正在执行,反之则没在执行。 - isFinished
: 返回YES表示操作执行成功或者被取消了,NSOperationQueue
只有当它管理的所有操作的isFinished
属性全标为YES以后操作才停止出列,也就是队列停止运行,所以正确实现这个方法对于避免死锁很关键。
取消
早些取消那些没必要的操作是十分有用的。取消的原因可能包括用户的明确操作或者某个相关的操作失败。
与之前的执行状态类似,当NSOperation
的-cancel
状态调用的时候会通过KVO通知isCancelled
的keypath来修改isCancelled
属性的返回值,NSOperation
需要尽快地清理一些内部细节,而后到达一个合适的最终状态。特别的,这个时候isCancelled
和isFinished
的值将是YES,而isExecuting
的值则为NO。
优先级
不可能所有的操作都是一样重要,通过以下的顺序设置queuePriority属性可以加快或者推迟操作的执行:
1 | NSOperationQueuePriorityVeryHigh |
此外,有些操作还可以指定threadPriority
的值,它的取值范围可以从0.0到1.0,1.0代表最高的优先级。鉴于queuePriority
属性决定了操作执行的顺序,threadPriority
则指定了当操作开始执行以后的CPU计算能力的分配,如果你不知道这是什么,好吧,你可能根本没必要知道这是什么。
依赖性
根据你应用的复杂度不同,将大任务再分成一系列子任务一般都是很有意义的,而你能通过NSOperation的依赖性实现。
比如说,对于服务器下载并压缩一张图片的整个过程,你可能会将这个整个过程分为两个操作(可能你还会用到这个网络子过程再去下载另一张图片,然后用压缩子过程去压缩磁盘上的图片)。显然图片需要等到下载完成之后才能被调整尺寸,所以我们定义网络子操作是压缩子操作的依赖,通过代码来说就是:
1 | [resizingOperation addDependency:networkingOperation]; |
除非一个操作的依赖的isFinished返回YES,不然这个操作不会开始。时时牢记将所有的依赖关系添加到操作队列很重要!!!
此外,确保不要意外地创建依赖循环,像A依赖B,B又依赖A,这也会导致死锁。
completionBlock
每当一个NSOperation
执行完毕,它就会调用它的completionBlock
属性一次,这提供了一个非常好的方式让你能在视图控制器(View Controller)里或者模型(Model)里加入自己更多自己的代码逻辑。比如说,你可以在一个网络请求操作的completionBlock
来处理操作执行完以后从服务器下载下来的数据。
自定义NSOperation
- 如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。
- 如果重写start方法,自行控制任务状态
自定义NSOperation
时,思路都是大同小异,大致可以分为一下几步:
创建一个集成自NSOperation的类
重写NSOperation的main()方法,在main()方法中实现耗时操作
然后使用时创建自定义的NNSOperation对象,把它添加到NSOperationQueue中,这样就可以自动执行了
其实,这只是自定义NSOperation的一种方法,而且是比较简单的一种方法,不需要自己去控制NSOperation的完成,取消等。另外一种方式是重写NSOperation的start方法,这种方法就需要你自己去控制NSOperation的完成,取消,执行等,而且有许多需要注意的地方。下面着重介绍一些第二种方法。
第一种方式像下面那样就可以了,执行完耗时操作后,这个operation会被自动dealloc
1 | - (void)main |
如果第二种方式就需要自己控制状态
1 | @interface DMOperation () |
重写了- (void)start
方法后,- (void)main
方法不会被调用
finished
设置为YES后才会调用dealloc
,这是只有finished
后,才会从NSOperationQueue
队列中移除。
GCD(Grand Central Dispatch)
barrier
dispatch_barrier_asyn
的作用是将加入并发队列中的任务,分成三部分,保证这三部分是按顺序执行的,每一部分内部可能是无序的。
实例代码如下:
1 | - (void)dispatch_barrier_1 |
注意:
dispatch_barrier_asyn使用时对队列的类型有要求,只能是通过dispatch_queue_create方法创建的并发队列才后有上面提到的功能,否则,相当于普通的dispatch_asyn。对于dispatch_barrier_syn也是一样的
dispatch_barrier_sync与dispatch_barrier_async
1、等待在它前面插入队列的任务先执行完
2、等待他们自己的任务执行完再执行后面的任务
不同点:
1、dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们
2、dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。
dispatch_barrier_asyn的应用
利用dispatch_barrier_asyn我们可以实现一个读写安全的模型,这样可以保证写的过程中不能被读,以免数据不对,但是读与读之间并没有任何的冲突
代码如下:
1 | @interface DMCache() |
group
dispatch_group_async 用来阻塞一个线程,直到一个或多个任务完成执行。进行线程同步。
代码如下:
1 | - (void)dispatch_group_1 |
dispatch_group_enter
和dispatch_group_leave
成对出现,可以实现异步任务的同步
1 | - (void)dispatch_group_2 |
dispatch_apply
dispatch_apply
函数是dispatch_sync
函数和Dispatch Group的关联API,该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部的处理执行结束
1 | - (void)dispatch_apply_1 |
semaphore
关于信号量,一般可以用停车来比喻:
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait
函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)
),
调用一次dispatch_semaphore_signal
,剩余的车位就增加一个;调用一次dispatch_semaphore_wait
剩余车位就减少一个;
当剩余车位为0时,再来车(即调用dispatch_semaphore_wait
)就只能等待。有可能同时有几辆车等待一个停车位。有些车主
没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,
所以就一直等下去。
可以使用semaphore
进行线程同步,将异步操作转换为同步操作
1 | - (void)dispatch_semaphore_1 |
也可以加锁
1 | //为线程加锁 |
还可以控制线程并发数
1 | //使用 Dispatch Semaphore 控制并发线程数量 |
Dispatch Source
Dispatch Source
是BSD系内核惯有功能kqueue
的包装。kqueue
是在 XNU 内核中发生各种事件时,在应用程序编程方执行处理的技术。其 CPU 负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理 XNU 内核中发生的各种事件的方法中最优秀的一种。
Dispatch Source
也使用在了 Core Foundation 框架的用于异步网络的API CFSocket 中。因为Foundation 框架的异步网络 API 是通过CFSocket
实现的,所以可享受到仅使用 Foundation 框架的 Dispatch Source
带来的好处。
那么优势何在?使用的 Dispatch Source
而不使用 dispatch_async
的唯一原因就是利用联结
的优势。
创建dispatch_source_t
1 | dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); |
参数:
参数 | 意义 |
---|---|
type | dispatch源可处理的事件类型 |
handle | 可以理解为索引、id或句柄,假如要监听进程,需要传入进程的ID |
mask | 可以理解为描述,提供更详细的描述,让它知道具体要监听什么 |
queue | 自定义源需要的一个队列,用来处理所有的响应句柄(block) |
Dispatch Source可处理的所有事件:
名称 | 意义 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 自定义的事件,变量增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 自定义的事件,变量OR |
DISPATCH_SOURCE_TYPE_MACH_SEND | MACH端口发送 |
DISPATCH_SOURCE_TYPE_MACH_RECV | MACH端口接收 |
DISPATCH_SOURCE_TYPE_PROC | 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号 |
DISPATCH_SOURCE_TYPE_READ | IO操作,如对文件的操作、socket操作的读响应 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收到UNIX信号时响应 |
DISPATCH_SOURCE_TYPE_TIMER | 定时器 |
DISPATCH_SOURCE_TYPE_VNODE | 文件状态监听,文件被删除、移动、重命名 |
DISPATCH_SOURCE_TYPE_WRITE | IO操作,如对文件的操作、socket操作的写响应 |
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE | 内存压力监听 |
自定义事件
上面类型中可以使用自定时间的类型只有DISPATCH_SOURCE_TYPE_DATA_ADD
和DISPATCH_SOURCE_TYPE_DATA_OR
实例:
当我们更新进度条时,可能在多个线程上同时做很多任务,每个任务完成后,刷新界面,更新一点进度条的进度,因为每个任务都更新一次进度条,造成界面刷新次数太多,可能会导致界面卡顿,所以此时利用Dispatch Source能很好的解决这种情况,因为Dispatch Source在刷新太频繁的时候会自动联结起来,下面就用代码实现一下这个场景。
1 |
|
联结方式为OR时
1 | - (void)dispatch_source_6 |
DISPATCH_SOURCE_TYPE_DATA_ADD
与 DISPATCH_SOURCE_TYPE_DATA_OR
的区别:
DISPATCH_SOURCE_TYPE_DATA_ADD 带别的方式为将dispatch_source_merge_data
联结进来的数据进行加运算
DISPATCH_SOURCE_TYPE_DATA_OR 带别的方式为将dispatch_source_merge_data
联结进来的数据进行或运算
比如上面的那个例子,先将usleep(2000);
这段代码注释掉以后,通过DISPATCH_SOURCE_TYPE_DATA_ADD
方式运行的到的结果会是 10(1+2+3+4),通过DISPATCH_SOURCE_TYPE_DATA_OR
方式运行得到的结果是7 (1|2|3|4)
如果将这段代码usleep(2000);
放开,不管是用ADD还是OR会打印四次结果为下面所示,这正是联结的作用
1 | 监听函数:1 |
DISPATCH_SOURCE_TYPE_VNODE
通过这种类型,可以监听文件状态
状态类型 | 状态 |
---|---|
DISPATCH_VNODE_DELETE | 文件被删除 |
DISPATCH_VNODE_WRITE | 文件数据发生变化 |
DISPATCH_VNODE_EXTEND | 文件size发生变化 |
DISPATCH_VNODE_ATTRIB | 文件metadata发生变化 |
DISPATCH_VNODE_LINK | 文件链接数发生变化 |
DISPATCH_VNODE_RENAME | 文件被重命名 |
DISPATCH_VNODE_REVOKE | 文件被revoked |
DISPATCH_VNODE_FUNLOCK | 文件被unlocked |
通过这个属性我们可以实现一个监听指定目录下文件内容是否发生变化,如果变化,我们可以来做一些操作,这里有一个我写工具LLDirWatchDog欢迎star
1 |
|
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE
1 | //内存压力情况变化. |
多线程与锁
iOS中锁的类型:
- @synchronized
- atomic(保证赋值操作)
OSSpinLock(自旋锁)
- 循环等待询问,不释放当前资源
- 用于轻量级数据访问,简单的int值+/-1的操作
NSLock
- NSRecursiveLock(递归锁)
- dispatch_semaphore_t(信号量)
1 | - (void)methodA |
上面代码会死锁,可用递归锁解决
QA
看下面代码的是否有问题及执行结果,然后分析结果
Q1:
1 | - (void)demo//task1 |
结果: 由于队列中任务的循环等待引起的死锁
分析:
1 | task1是在主队列中一个任务,被派发在主线程中执行,而task2也被加入主队列, |
Q2:1
2
3
4
5
6
7
8- (void)demo{//task1
dispatch_queue_t q = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
NSLog(@"outer--1");
dispatch_sync(q, ^{//task2
NSLog(@"inner--1");
});
NSLog(@"outer--2");
}
结果:
1 | outer--1 |
分析:
1 | task1 和 task2 分别在主队列和serial串行队列中,不存在队列中任务的循环等待的问题 |
Q3:1
2
3
4
5
6
7
8
9
10
11- (void)demo{
dispatch_queue_t q = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
NSLog(@"outer--1");
dispatch_async(q, ^{//开辟了一条子线程 task1
NSLog(@"inner--1--%@",[NSThread currentThread]);
dispatch_sync(q, ^{//任务提交到串行队列中,并在当前子线程中执行 task2
NSLog(@"inner--2");
});
});
NSLog(@"outer--2");
}
结果:由于队列中任务的循环等待引起的死锁
分析:
1 | 串行队列中加入task1 |
Q4:
1 | - (void)demo{ |
结果:
1 | outer--1 |
分析:
1 | task1加入串行队列 |
Q5:
1 | - (void)demo{ |
结果:
1 | outer--1 |
分析:
1 | task1 任务被添加到并行队列中后,开始执行 |
Q6:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29- (void)demo
{
dispatch_queue_t q = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(q, ^{
NSLog(@"1");
[self performSelector:@selector(printLog) withObject:nil afterDelay:0];
NSLog(@"3");
});
/*
GCD维持的线程池 其中的线程没有开启runloop,
performSelector 方法调用所在的当前的线程,必须开启runloop
*/
}
与demo1效果一样
- (void)demo1
{
__weak typeof(self) weakSelf = self;
[NSThread detachNewThreadWithBlock:^{
NSLog(@"1:%@",[NSThread currentThread]);
[weakSelf performSelector:@selector(printLog) withObject:nil afterDelay:0];
NSLog(@"3:%@",[NSThread currentThread]);
}];
}
- (void)printLog
{
NSLog(@"2");
}
结果
1 | 1 |
分析:
1 | 1、performSelector 调用该方法所在的线程,必须开启runloop,否则不会执行 |