iOS 多线程总结 - 基本概念 GCD NSOperation
在介绍多线程编程的时候我们需要明确一般会在什么场合上使用多线程,我们知道每个进程一定有一个线程–主线程,在这个线程中一般用于更新界面相关的任务,一般任务又可以分成耗时的和非耗时操作,计算密集型的任务和IO密集型任务就属于耗时任务,比如读写数据库,读写磁盘文件,访问网络等,这些一般放在子线程中完成,但是一般在任务完成等时候都会将结果呈现在界面上,这时候就需要在主线程中完成。这个大家应该都知道,但是往往很多人会有误区,是不是线程越多越好,答案是否定的,创建的线程过多有如下问题:
- 从空间角度来看:每个线程都需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程切换需要上下文切换,这就需要耗费一定的时间,线程越多,CPU在调度线程上的开销就越大,同样会降低程序的性能
- 线程越多,线程关系越复杂,线程竞争,线程管理,以及死锁等其他多线程问题发生的概率就会相应的增加。
因此合理得管理多线程是十分必要的工作。
下面将从:
1.多线程基本概念
2.线程通讯
3.线程同步,线程安全
三个大方面对iOS多线程技术进行一个简要的总结
1.多线程基本概念
1.1 多线程编程的基本概念
进程: 进程是指系统中正在运行的一个应用程序,进程之间是独立的,有自己专用且受保护的内存空间。
线程: 是操作系统能够进行运算调度的最小单位,是一个CPU执行的一条无分叉的命令序列,进程是由至少一个线程(主线程)构成,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间、文件描述符等。但每个线程都拥有自己的栈,寄存器,本地存储
并行,串行: 是针对线程队列的,表示一次可以执行多少个线程,串行队列每次只能执行一个线程,并行队列可以同时执行多个线程。
同步,异步: 是针对线程行为的,指明线程的执行是否要等到任务返回后再往下执行。
线程安全: 指代码在多线程或者并发任务下能够被安全调用,而不会引起任何问题
线程生命周期:
线程生命周期可以分成如下5个阶段:
- NEW - [新建状态] 表示线程被新建的状态
- RUNNABLE - [可运行状态] 新建的线程并不一定会马上被CPU调度,而是进入一个中间状态RUNNABLE状态,等待被CPU调度,处于阻塞状态的线程恢复后不会立刻切换到RUNNING状态,而是先切换到RUNNABLE状态。
- RUNNING - [正在运行状态] 当CPU调度发生,并任务队列中选中了某个RUNNABLE线程时,该线程会进入RUNNING 执行状态。
- BLOCKED - [阻塞状态] 由于调用了睡眠,IO阻塞,等待锁的时候会处于阻塞状态。
- TERMINATED - [终结状态] 终结状态是线程的最终状态,处于此状态中的线程不会切换到以上任何状态,一旦线程进入了终结状态,就意味着这个线程生命的终结。
我们来简单介绍下线程这五种状态的切换图:
新创建的线程会进入NEW状态,如果有线程正在运行则处于NEW状态的线程会转换到RUNNABLE状态,等待被CPU调度,一旦正在运行的线程让出了CPU时间,CPU就会从处于RUNNABLE状态的线程中取出优先级最高的线程,进入RUNNING状态,处于RUNNING状态的线程一旦调用了睡眠,IO阻塞,等待锁的时候会处于BLOCKED状态。一旦阻塞的条件解除了就会进入RUNABLE状态,不论是处于RUNNING状态还是RUNNABLE状态,还是BLOCKED状态只要调用了stop方法,就会进入TERMINATED状态。
1.2 iOS多线程实现方案对比
iOS 多线程方案有如下几种:
pthread
语言: C 语言
优点:跨平台,可移植
缺点:需要自己管理线程生命周期,所以用得比较少NSThread
语言: OC 语言
优点:是针对pthread的面向对象的封装
缺点:需要自己管理线程生命周期,使用上还是显得比较麻烦,一般用于查看当前线程状态等不涉及线程周期的场景。GCD
语言: C 语言
优点:能够充分发挥多核的特性,自动管理线程生命周期不需要手动管理NSOperation&&NSOperationQueue
语言: OC 语言
优点:基于GCD 底层的面向对象封装,添加了线程依赖,并发数控制等功能
1.3 iOS多线程组成:
iOS平台多线程的组成如下图所示:
顶层包括两大部分,一部分是基于OC语言的NSThread和NSOperationQueue类,它们建立在Core Services层的Foundation框架之上,同时也提供了一套基于C语言的GCD线程池函数库来支持多线程的处理应用,这两部分的底层都是基于POSIX标准中的pthread线程库。用户态下的线程创建通过系统调用到达内核态的BSD层并创建bsdthread对象,而BSD层则调用Mach层的ksthread对象来完成最终线程的创建和调度的。
1.4 NSThread的使用
NSThread的创建:
/* |
NSThread常见属性:
//只读属性,线程是否在执行 |
NSThread常用方法:
[thread start]; 启动线程 |
线程之间的通信:
// 在主线程上执行操作 |
1.5 GCD 的使用
使用GCD 需要明确:需要执行哪些操作,要投递到哪种分发队列,怎么执行这些任务串行还是并行。它有两个核心概念“任务”和“队列”,我们只需专注于想要执行的“任务” block,然后添加到适当的“队列”中,剩余的多线程生命周期管理以及多CPU任务分配问题都是GCD来替我们完成。
1.5.1 GCD 的队列类型
GCD 有两大类队列:
- 串行队列(Serial Dispatch Queue):
串行队列每次只能执行一个任务,但是在应用中可以创建多个串行队列
dispatch_queue_t queue = dispatch_queue_create(“com.idealist.test”, DISPATCH_QUEUE_SERIAL); |
iOS 默认创建的主线程就是串行队列,获取串行队列可以通过如下方法获取:
dispatch_get_main_queue() |
串行队列的一个很重要的用途就是用于解决数据竞争,因为处于同一个串行队列中两个任务不可能并发运行,所以就没有可能会同时访问同一个临界区的风险。所以仅对于这些任务而言,这种运行机制能够保护临界区避免发生竟态条件
- 并行队列(Concurrent Dispatch Queue):
并行队列和散弹枪一样每次可以同时执行多个任务,但是在系统中对同时执行的任务数是有限制的,这取决于CPU核数以及CPU负载等因素决定。
dispatch_queue_t queue = dispatch_queue_create(“com.idealist.test”, DISPATCH_QUEUE_CONCURRENT); |
和串行队列一样iOS系统为创建并行队列增加了全局并行队列:
dispatch_get_global_queue() |
全局队列有四个优先级:
DISPATCH_QUEUE_PRIORITY_HIGHT 高优先级 |
1.5.2 任务派发函数
dispatch_sync(queue, ^{ |
dispatch_sync 函数将一个任务添加到一个队列中,会阻塞当前线程,直到该任务执行完毕。dispatch_async 不会等待任务执行完,当前线程会继续往下走,不会阻塞当前线程。
在使用的时候需要特别注意不要往当前队列中使用dispatch_sync抛任务,这样很容易造成死锁。
1.5.3 GCD 停止和恢复
dispatch_suspend(queue) //暂停某个队列 |
1.5.4 任务和队列的搭配情况
我们知道任务有同步任务和异步任务,队列有串行队列和并行队列之分。所以具体就有4种组合:
在介绍这四种方式的行为之前大家要记住一点,同步方式是不会创建新线程的,异步方式会创建新线程。
- [同步 + 串行]:
这种方式没有开启新线程,串行执行任务,并且要注意这种情况很容易造成死锁,如果一个消息队列向自己消息队列中投放任务这时候就会造成死锁。
dispatch_queue_t disqueue = dispatch_queue_create("com.idealist.test", DISPATCH_QUEUE_SERIAL); |
只有在发送任务的队列和任务队列不是同一个队列的时候才会正常执行,注意这里是在同一个线程
dispatch_queue_t disqueue = dispatch_queue_create("com.idealist.disqueue", DISPATCH_QUEUE_SERIAL); |
这种用得不多。
- [异步 + 串行]:
这种情况只会生成一个线程,同一个队列的任务在同一个线程执行。
dispatch_queue_t disqueue = dispatch_queue_create("com.idealist.disqueue", DISPATCH_QUEUE_SERIAL); |
这种比较常用,在不阻塞工作线程外,还能避免资源的竞争。
- [同步 + 并行]:
这种情况下没有开启新的线程,并且任务也是一个个运行的。虽然并发队列可以开启多个线程同时执行多个任务。但是因为同步任务不具备开启新线程的能力,只有当前线程这一个线程,所以也就不存在并发。而且同步任务需要等待队列的任务执行结束之后,才能继续接着执行下面的操作,所以任务只能一个接一个按顺序执行,不能同时执行。
- [异步 + 并行]:
这种情况下可开启多个线程,同时执行多个任务,但是这种由于任务并行的顺序不确定性,会很容易出错。
1.5.5 GCD 任务组 (dispatch_group)
- “对于并行队列,以及多个串行、并行队列混合的情况我们如何知道所有任务都已经执行完了”
- “如何在某些任务执行完毕后,执行一个操作“
遇到这种情况我们就可以使用GCD 任务组来解决,GCD 任务组 能够在任务组中的任务执行完毕后,执行某个任务。
//创建调度组 |
dispatch_group_async(group, queue, ^{ |
等价于
dispatch_group_enter(group); |
dispatch_group 例子:
dispatch_group_t group = dispatch_group_create(); |
1.5.5 GCD 其他方法
- GCD 栅栏方法:dispatch_barrier_async
要异步执行多组操作,且前一组操作执行完之后,才能开始执行后一组操作。这种情况就需要用到栅栏来隔离。
dispatch_queue_t queue = dispatch_queue_create("com.dnduuhn.test", DISPATCH_QUEUE_CONCURRENT); |
在上面任务中任务3,任务4 会在 任务1,任务2之后执行。
- GCD 延时执行方法:dispatch_after
延迟一段时间后执行某个操作:
dispatch_after函数传入的时间参数,并不是指在这时间之后开始执行处理,而是在指定时间之后将任务追加到队列中。并且这个时间不是绝对准确时间,但是可以满足对时间不是很严格的延迟要求。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ |
- GCD 一次性代码:dispatch_once
使用 dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。
static dispatch_once_t onceToken; |
- GCD 快速迭代方法:dispatch_apply
dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
上面会跑6个任务,这6个任务同时并行,index代表第几个任务。这些任务是无序得并发执行。
- GCD 信号量: Dispatch Semaphore
信号量是持有计数的信号,使用它控制对有限资源的使用和访问。假设有一间房子,它对应一个进程,房子里的两个人就对应两个线程。这个房子(进程)有很多资源,比如花园、客厅、卫生间等,是所有人(线程)共享的。但是有些地方,比卫生间,最多只能有1个人能进去。怎么办呢,在卫生间门口挂1把钥匙。进去的人(线程)拿着钥匙进去(信号量 -1),外面的人(线程)没有钥匙就在门口等待,直到里面的人出来并把钥匙重新放回门口(信号量+1),此时外面等待的人再拿着这个钥匙进去,所有人(线程)就按照这种方式依次访问卫生间这个有限的资源。门口的钥匙数量就称为信号量(Semaphore)。信号量为0时需要等待,信号量不为零时,减去1而且不等待。
|
例子:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
1.6 NSOperation && NSOperationQueue的使用
下图中列举了NSOperation 以及NSOperationQueue的一些重要概念:
在介绍NSOperation之前我们需要知道NSOperation其实是GCD的一种封装,但是它的调度形式和GCD有着明显区别,GCD中的调度是以FIFO形式进行调度的,但是添加到NSOperationQueue中的任务会先进入RUNABLE状态,然后按照操作的优先级进行调度,并且通过设置队列的最大并发数来控制任务队列的串行,并行行为。
****1.6.1 创建操作NSOperation ****
NSOperation 有三种方式可以创建,一种是NSInvocationOperation,一种是NSBlockOperation,还有一种通过自定义NSOperation
NSInvocationOperation:
// 1.创建 NSInvocationOperation 对象 |
这种形式任务会运行在当前线程。
NSBlockOperation:
NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。
// 1.创建 NSBlockOperation 对象 |
通过 addExecutionBlock 添加额外操作,这些操作(包括blockOperationWithBlock中的操作)可以在不同的线程中并发执行。只有当所有相关的操作已经完成执行时,才视为操作完成
// 1.创建 NSBlockOperation 对象 |
自定义NSOperation:
@interface IDLOperation : NSOperation |
|
1.6.2 创建操作队列
NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
NSOperationQueue *queue = [NSOperationQueue mainQueue]; |
自定义队列创建方法:添加到这种队列中的操作,就会自动放到子线程中执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; |
1.6.4 将操作添加到操作队列
addOperation
[operationQueue addOperation:op1]; |
addOperationWithBlock
[operationQueue addOperationWithBlock:^{ |
通过上述两种方式将操作加入到操作队列后能够开启新线程,进行并发执行
1.6.3 设置操作队列属性
最大并发操作数:maxConcurrentOperationCount
- maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
- maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行,一个操作完成之后,下一个操作才开始执行。
- maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,可以同时执行多个操作。开启线程数量是由系统决定的,不需要我们来管理。
我们一般通过maxConcurrentOperationCount来控制操作队列的串并行执行顺序。
1.6.4 设置操作间依赖,及优先级,启动操作
- 设置操作间依赖
NSOperation 还有一个比较强大的功能就是可以设置操作直接的依赖,依赖的操作会等被依赖的操作执行完毕后执行。
- (void)addDependency:(NSOperation *)op; 添加依赖,在操作op完成之后才执行当前操作。 |
还可以通过
@property (readonly, copy) NSArray<NSOperation *> *dependencies; |
来获取当前操作开始执行之前完成执行的所有操作对象数组。
- 设置操作优先级
优先级的取值如下,可以通过****setQueuePriority:****方法来设置优先级。
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) { |
优先级只是一个参考,如果一个低优先级的任务,准备就绪了,但是一个高优先级的尚未准备就绪,就会先跑低优先级的任务。
1.6.5 NSOperation 线程切换
NSOperationQueue *queue = [[NSOperationQueue alloc]init]; |