进程、线程
进程
- 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单位;
- 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解成手机里的一个App;
- 每个进程之间都是独立的,每个进程运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源;
线程
- 程序执行流的最小单元,线程是进程中的一个实体;
- 一个进程想要执行任务,必须至少有一条线程,应用程序启动的时候,系统会默认开启一条线程,也就是主线程;
进程和线程的关系
- 线程是进程的执行单元,进程的所有任务都在线程中执行;
- 线程是CPU分配和调度资源的最小单位;
- 一个程序可以对应多个进程(多进程),一个进程可以对应多个线程,但至少要有个线程;
- 同个进程内的线程共享进程资源;
多线程
- 统一时间,CPU 只能处理一条线程,只有一条线程在执行,多线程并发执行,其实是 CPU 快速的在多条线程之间调度 (切换),如果 CPU 调度线程时间足够快,就造成了多线程并发执行的假象;
- 如果线程非常非常多,CPU 会在 N 多线程之间调度,消耗大量的 CPU 资源, 每条线程被 调度执行的频次会降低 (线程的执行效率降低)
- 多线程的优点:
- 能适当提高程序的执行效率
- 能适当提高资源利用率(CPU、内存利用率)
- 多线程的缺点:
- 开启线程需要占用一定的内存空间(默认情况下,主线程占用 1M,子线程占用 512KB),如果开启大量的 线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调度线程上的开销就越大
- 程序设计更加复杂:比如线程之间的通信、多线程的数据共享
任务、队列
任务
就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种 方式:同步执行(sync)和异步执行(async)
同步(Sync): 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任 务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具 备开启新线程的能力。
异步(Async): 线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中 执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执 行任务
队列
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的 线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队 列的头部开始读取。每读取一个任务,则从队列中释放一个任务
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是: 执行顺序不同,以及开启线程数不同
- 串行队列(SerialDispatchQueue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只 开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系 统自动为我们创建的 - 并发队列(ConcurrentDispatchQueue):
同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有 在异步(dispatch_async)函数下才有效
iOS 中的多线程
- 主要有三种:NSThread、NSoperationQueue、GCD
是我们自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造器方式 它就会自动启动。只要是我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使 用完毕后的资源回收
1 | NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数" ]; |
performSelector…只要是 NSObject 的子类或者对象都可以通过调用方法进入子线程和主线程,其实这些方法 所开辟的子线程也是 NSThread 的另一种体现方式。 在编译阶段并不会去检查方法是否有效存在,如果不存在只会给出警告;
GCD 对比 NSOperationQueue
我们要明确 NSOperationQueue
与 GCD
之间的关系GCD
是面向底层的C语言的 API
, NSOperationQueue
是用 GCD
构建封装的, 是 GCD
的高级抽象。
GCD
执行效率更高,而且由于队列中执行的是由block
构成的任务,这是一个轻量级的数据结构,写起来更方便GCD
只支持FIFO
(先进先出)的队列, 而NSOperationQueue
可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序;NSOperationQueue
甚至可以跨队列设置依赖关系, 但是GCD
只能通过设置串行队列,或者在队列内添加barrier
(dispatch_barrier_async
) 任务, 才能控制执行顺序,较为复杂NSOperationQueue
因为面向对象,所以支持KVO
, 可以检测Operation
是否正在执行 (isExecuted)、 是否结束 (isFinished) 、 是否取消 (isCanceld);
实际项目开发中,很多时候只会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇且优化完善,运行快速的
GCD
作为首选;
如果考虑异步操作之间的事务性,顺序性,依赖关系,比如多线程并发下载,GCD 需要自己写更多的代码来实现, 而NSOperationQueue
已经内建了这些支持
无论是GCD
还是NSOperationQueue
,我们接触的都是任务和队列,没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做的很好。而NSThread
需要我们自己去管理线程的生命周期,还需要考虑线程同步,加锁问题,造成一些性能上的开销
GCD – 队列
iOS 中有 GCD
、 NSOperationQueue
、 NSThread
等多线程技术方案
而 GCD
共有三种队列类型:
- main queue: 通过 dispatch_get_main_queue()获得,这是一个与主线程祥光的串行队列。
- global queue: 全局队列,是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列,调用
dispatch_get_global_queue
并传入优先级来访问队列 - 自定义队列: 通过函数
dispatch_queue_create
穿件的队列
死锁
概念: 死锁就是队列引起的循环等待
1、 一个比较常见的死锁例子: 主队列同步操作
1 | - (void)viewDidLoad { |
在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。 同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务, viewDidLoad 才会继续向下执行。
而 viewDidLoad 和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待 viewDidLoad 执行完毕 后才能继续执行,viewDidLoad 和这个任务就形成了相互循环等待,就造成了死锁。 想避免这种死锁,可以将同步改成异步 dispatch_async,或者将 dispatch_get_main_queue 换成其他串行或并行 队列,都可以解决。
2、同样下面的代码也会造成死锁:
1 | dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); |
外面的函数无论是同步还是异步都会造成死锁。
这是因为里面的任务和外面的任务都在同一个 serialQueue 队列内,又是同步,这就和上边主队列同步的例 子一样造成了死锁
解决方法也和上边一样,将里面的同步改成异步 dispatch_async,或者将 serialQueue 换成其他串行或并行队 列,都可以解决
1 | dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); |
这样是不会死锁的,并且 serialQueue 和 serialQueue2 是在同一个线程中的。
GCD 任务执行顺序
- 串行队列先异步后同步
1 | dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); |
打印顺序是 13245
首先先打印 1
接下来将任务 2 其添加至串行队列上,由于任务 2 是异步,不会阻塞线程,继续向下执行,打印 3 然后是任务 4,将任务 4 添加至串行队列上,因为任务 4 和任务 2 在同一串行队列,根据队列先进先出原则, 任务 4 必须等任务 2 执行后才能执行,又因为任务 4 是同步任务,会阻塞线程,只有执行完任务 4 才能继 续向下执行打印 5
所以最终顺序就是 13245。
这里的任务 4 在主线程中执行,而任务 2 在子线程中执行。
如果任务 4 是添加到另一个串行队列或者并行队列,则任务 2 和任务 4 无序执行(可以添加多个任务看效果)
如果 GCD 里添加 performSelector 会怎么样呢?
1 |
|
这里的 test 方法是不会去执行的,原因在于 - (void)performSelector: (SEL)aSelector withObject: (nullable id)anArgument afterDelay: (NSTimeInterval)delay;
这个方法要提交任务到runloop上的,然 GCD 地城创建的线程,默认是没有开启对应的runloop的,所以这个方法会失效;
而如果将 dispatch_get_global_queue 改成主队列,由于主队列所在的主线程是默认开启了 runloop 的,就会 去执行(将 dispatch_async 改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test 方法也 是会去执行的)。
- dispatch_barrier(栅栏函数)
- dispatch_barrier_sync:Submitsabarrierblockobjectforexecutionandwaitsuntilthatblockcompletes.(提交 一个栅栏函数在执行中,它会等待栅栏函数执行完)
- dispatch_barrier_async:Submitsabarrierblockforasynchronousexecutionandreturnsimmediately.(提交一 个栅栏函数在异步执行中,它会立马返回)
- dispatch_barrier_sync 和 dispatch_barrier_async 的区别也就在于会不会阻塞当前线程
实现多读单写:
1 | - (id)readDataForKey: (NSString *)key { |
- dispatch_group_async
使用场景: 在 n 个耗时并发任务都完成后,再执行接下来的任务。比如,在 n 个网络请求完成后去刷新UI
1 |
|
- dispatch_semaphore (信号量)
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。
Dispatch Semaphore 提供三个函数: - dispatch_semaphore_create: 创建一个Semaphore 并初始化信号的总量;
- dispatch_semaphore_signal: 发送一个信号,让信号总量 + 1;
- dispatch_semaphore_wait: 可以使总信号量 -1, 单信号总量为0 时就会一直等待
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
1、 保证线程同步:
1 | dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); |
dispatch_semaphore_wait 加锁阻塞了当前线程,dispatch_semaphore_signal 解锁后当前线程继续执行
2、 保证线程安全,为线程加锁
在线程安全中可以将 dispatch_semaphore_wait 看作加锁,而 dispatch_semaphore_signal 看作解锁 首先创建全局变量
1 | _semaphore = dispatch_semaphore_create(1); |
注意到这里的初始化信号量是 1。
1 | - (void)asyncTask { |
异步并发调用 asyncTask
1 | for (NSInteger i = 0; i < 100; i++) { |
打印任务是从1 顺序执行到100, 没有发生两个任务同时执行的情况
原因如下:
在子线程中并发执行 asyncTask,那么第一个添加到并发队列里的,会将信号量减 1,此时信号量等于 0, 可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于 0,必须等当前正在执行的任务 执行完毕后调用 dispatch_semaphore_signal 将信号量加 1,才可以继续执行接下来的任务,以此类推, 从而达到线程加锁的目的。
- 延时函数 (dispatch_after)
dispatch_after 能让我们添加进队列的任务延时执行,该行数并不是指在指定时间后执行处理,而只是在指定时间追加处理到 dispatch_queue
1 | // 第一个参数是time, 第二个参数是 dispatch_queue, 第三个参数是要执行的block |
由于其内部使用的是 dispatch_time_t 管理时间,而不是 NSTimer.
所以如果在子线程中调用,相比 performSelector: afterDelay, 不用关系 runloop 是否开启
- 使用 dispatch_once 实现单例
1 | + (instancetype)shareInstance { |
NSOperationQueue 的优缺点
NSOperation
、NSOperationQueue
是苹果提供给我们的一套多线程解决方案。实际上 NSOperation
、 NSOperationQueue
是基于 GCD
更高一层的封装,完全面向对象。但是比 GCD
更简单易用、代码可读 性也更高。
1、可以添加任务依赖,方便控制执行顺序;
2、 可以设定操作执行的优先级;
3、 任务执行状态控制: isReady
, isExecuting
, isFinished
, isCancelled
;
如果只是重写 NSOperation
的 main
方法, 由底层控制变更执行任务及完成状态,以及任务退出如果重写了 NSOperation
的 start
方法, 自行控制任务状态
系统通过 KVO 的方式一处 isFinished == YES
的 NSOperation
4、 可以设置最大并发量
NSOperation 和 NSOperationQueue
操作 (Operation):
执行操作的意思,换句话说就是你在线程中执行的那段代码。
在 GCD 中是放在 block 中的。在 NSOperation 中,使用NSOperation
子类NSInvocationOperation
、NSBlockOperation
,或者自定义子类来封装操作。操作队列 (Operation Queues)
这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。
NSOperationQueue
对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依 赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优 先级是操作对象自身的属性)。操作队列通过设置最大并发操作数(
maxConcurrentOperationCount
)来控制并发、串行。NSOperationQueue
为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,
而自定义队列在后台执行。
常驻线程的实现
NSThread 在实际开发中比较常用到的场景就是去实现常驻线程。 本质是开启 RunLoop 监听,实现线程常驻
- 由于每次开辟子线程都会消耗CPU, 在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的CPU,而且创建线程都是任务执行完成后也就释放了,不能再次利用,那么如何创建一个线程可以让它可以再次工作呢? 也就是创建一个常驻线程。
首先常驻线程既然是常驻,那么我们可以用 GCD 实现一个单例来保存 NSThread
1 | + (NSThread *) shareInstance { |