iOS多线程深入解析
必要的概念
进程/线程
进程:进程指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
线程:线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。
操作系统引入进程的目的:为了使多个程序能并发执行,以提高资源的利用率和系统的吞吐量。
操作系统引入线程的目的:在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS
具有更好的并发性。多线程技术可以提高程序的执行效率。
在引入线程的OS中,通常把进程作为资源分配的基本单位,而把线程作为独立运行和独立调度的基本单位。
同步/异步
同步:多个任务情况下,一个任务A执行结束,才可以执行另一个任务B。
异步:多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。存在多条线程。
并行/并发
并行:指两个或多个事件在同一时刻发生。多核CUP同时开启多条线程供多个任务同时执行,互不干扰。
并发:指两个或多个事件在同一时间间隔内发生。可以在某条线程和其他线程之间反复多次进行上下文切换,看上去就好像一个CPU能够并且执行多个线程一样。其实是伪异步。
线程间通信
在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
线程间通信的体现:
- 1个线程传递数据给另1个线程
- 在1个线程中执行完特定任务后,转到另1个线程继续执行任务
多线程概念
多线程是指在软件或硬件上实现多个线程并发执行的技术。通俗讲就是在同步或异步的情况下,开辟新线程,进行线程间的切换,以及对线程进行合理的调度,做到优化提升程序性能的目的。
多线程的优点
- 能适当提高程序的执行效率
- 能适当提高资源利用率(CPU、内存利用率)
- 避免在处理耗时任务时造成主线程阻塞
多线程的缺点
- 开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU在调度线程上的开销就越大
- 可能会导致多个线程相互持续等待[死锁]
- 程序设计更加复杂:比如线程之间的通信、多线程之间的数据竞争
GCD(Grand Central Dispatch)
Dispatch
会自动的根据CPU
的使用情况,创建线程来执行任务,并且自动的运行到多核上,提高程序的运行效率。对于开发者来说,在GCD
层面是没有线程的概念的,只有队列(queue
)。任务都是以block
的方式提交到队列上,然后GCD
会自动的创建线程池去执行这些任务。
GCD的优点:
- GCD是苹果公司为多核的并行运算提出的解决方案
- GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
GCD中有两个核心概念
任务 block:执行什么操作
队列 queue:用来存放任务
GCD的使用就两个步骤
- 定制任务,确定想做的事情
- 将任务添加到队列中,
GCD
会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的FIFO
原则:先进先出,后进后出。
GCD中有两个执行任务的函数
同步执行任务(sync)
1
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
> 同步任务会阻塞当前线程,然后把`Block`中的任务放到指定的队列中执行,只有等到`Block`中的任务完成后才会让当前线程继续往下运行。
> `sync`是一个强大但是容易被忽视的函数。使用`sync`,可以方便的进行线程间同步。但是,有一点要注意,`sync`容易造成死锁。
异步执行任务(async)
1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
> 异步任务会再开辟一个线程,当前线程继续往下走,新线程去执行`block`里的任务。
GCD的队列可以分为两大类型
并行队列(Concurrent Dispatch Queue):
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并行功能只有在异步(dispatch_async)函数下才有效
放到并行队列的任务,如果是异步执行,
GCD
也会FIFO
的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD
会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。
串行队列(Serial Dispatch Queue):
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
| |同步执行|异步执行|
|:–|:–|:–|
|串行队列|当前线程,一个一个执行|其他线程,一个一个执行|
|并发队列|当前线程,一个一个执行|开很多线程,一起执行|
Swift4 GCD 使用
DispatchQueue
最简单的,可以按照以下方式初始化一个队列
1 | //这里的名字能够方便开发者进行Debug |
这样初始化的队列是一个默认配置的队列,也可以显式的指明对列的其他属性
1 | let label = "com.leo.demoQueue" |
这里,我们来一个参数分析他们的作用
label
: 队列的标识符,方便调试qos
: 队列的quality of service
。用来指明队列的“重要性”,后文会详细讲到。attributes
: 队列的属性。类型是DispatchQueue.Attributes
,是一个结构体,遵循了协议OptionSet
。意味着你可以这样传入第一个参数[.option1,.option2]
。- 默认:队列是串行的。
.concurrent
:队列为并行的。.initiallyInactive
:则队列任务不会自动执行,需要开发者手动触发。
autoreleaseFrequency
: 顾名思义,自动释放频率。有些队列是会在执行完任务后自动释放的,有些比如Timer
等是不会自动释放的,是需要手动释放。
队列分类
- 系统创建的队列
- 主队列(对应主线程)
- 全局队列
- 用户创建的队列
1 | // 获取系统队列 |
suspend / resume
Suspend可以挂起一个线程,就是把这个线程暂停了,它占着资源,但不运行。
Resume可以继续挂起的线程,让这个线程继续执行下去。
1 | concurrentQueue.resume() |
QoS(quality of service)
QoS
的全称是quality of service
。在Swift 3
中,它是一个结构体,用来制定队列或者任务的重要性。
何为重要性呢?就是当资源有限的时候,优先执行哪些任务。这些优先级包括 CPU 时间,数据 IO 等等,也包括 ipad muiti tasking(两个App同时在前台运行)。
通常使用QoS
为以下四种,从上到下优先级依次降低。
User Interactive
: 和用户交互相关,比如动画等等优先级最高。比如用户连续拖拽的计算User Initiated
: 需要立刻的结果,比如push
一个ViewController
之前的数据计算Utility
: 可以执行很长时间,再通知用户结果。比如下载一个文件,给用户下载进度。Background
: 用户不可见,比如在后台存储大量数据
通常,你需要问自己以下几个问题
- 这个任务是用户可见的吗?
- 这个任务和用户交互有关吗?
- 这个任务的执行时间有多少?
- 这个任务的最终结果和UI有关系吗?
在GCD中,指定QoS有以下两种方式
方式一:创建一个指定QoS
的queue
1 | let backgroundQueue = DispatchQueue(label: "com.geselle.backgroundQueue", qos: .background) |
方式二:在提交block
的时候,指定QoS
1 | queue.async(qos: .background) { |
DispatchGroup
DispatchGroup用来管理一组任务的执行,然后监听任务都完成的事件。比如,多个网络请求同时发出去,等网络请求都完成后reload UI
。
1 | let group = DispatchGroup() |
group.notify
会等group
里的所有任务全部完成以后才会执行(不管是同步任务还是异步任务)。
Group.enter / Group.leave
1 | /* |
Group.wait
DispatchGroup
支持阻塞当前线程,等待执行结果。
1 | //在这个点,等待三秒钟 |
DispatchWorkItem
上文提到的方式,我们都是以block(
或者叫闭包)的形式提交任务。DispatchWorkItem
则把任务封装成了一个对象。
比如,你可以这么使用
1 | let item = DispatchWorkItem { |
也可以在初始化的时候指定更多的参数
1 | let item = DispatchWorkItem(qos: .userInitiated, flags: [.enforceQoS,.assignCurrentContext]) { |
DispatchWorkItemFlags
的参数分为两组
执行情况
- barrier
- detached
- assignCurrentContext
QoS覆盖信息
- noQoS //没有 QoS
- inheritQoS //继承 Queue 的 QoS
- enforceQoS //自己的 QoS 覆盖 Queue
after(延迟执行)
GCD
可以通过asyncAfter
来提交一个延迟执行的任务
比如
1 | let deadline = DispatchTime.now() + 2.0 |
延迟执行还支持一种模式DispatchWallTime
1 | let walltime = DispatchWallTime.now() + 2.0 |
这里的区别就是
DispatchTime
的精度是纳秒DispatchWallTime
的精度是微秒
Synchronization 同步
通常,在多线程同时会对一个变量(比如NSMutableArray
)进行读写的时候,我们需要考虑到线程的同步。举个例子:比如线程一在对NSMutableArray
进行addObject
的时候,线程二如果也想addObject
,那么它必须等到线程一执行完毕后才可以执行。
实现这种同步有很多种机制
NSLock 互斥锁
1 | let lock = NSLock() |
使用锁有一个不好的地方就是:lock
和unlock
要配对使用,不然极容易锁住线程,没有释放掉。
sync 同步函数
使用GCD
,队列同步有另外一种方式- sync
,讲属性的访问同步到一个queue
上去,就能保证在多线程同时访问的时候,线程安全。
1 | class MyData{ |
Barrier 线程阻断
假设我们有一个并发的队列用来读写一个数据对象。如果这个队列里的操作是读的,那么可以多个同时进行。如果有写的操作,则必须保证在执行写入操作时,不会有读取操作在执行,必须等待写入完成后才能读取,否则就可能会出现读到的数据不对。这个时候我们会用到 Barrier
。
以barrier flag
提交的任务能够保证其在并行队列执行的时候,是唯一的一个任务。(只对自己创建的队列有效,对gloablQueue
无效)
我们写个例子来看看效果
1 | let concurrentQueue = DispatchQueue(label: "com.leo.concurrent", attributes: .concurrent) |
然后,看到Log
1 | 2017-01-06 17:14:19.690 Dispatch[15609:245546] Start data task1 |
执行的效果就是:barrier
任务提交后,等待前面所有的任务都完成了才执行自身。barrier
任务执行完了后,再执行后续执行的任务。
Semaphore 信号量
DispatchSemaphore
是传统计数信号量的封装,用来控制资源被多任务访问的情况。
简单来说,如果我只有两个usb端口,如果来了三个usb请求的话,那么第3个就要等待,等待有一个空出来的时候,第三个请求才会继续执行。
我们来模拟这一情况:
1 | public func usbTask(label:String, cost:UInt32, complete:@escaping ()->()){ |
log
1 | 2017-01-06 15:03:09.264 Dispatch[5711:162205] Start usb task2 |
Tips:在
serial queue
上使用信号量要注意死锁的问题。感兴趣的同学可以把上述代码的queue
改成serial
的,看看效果。