Swift 并发编程(一)GCD篇
引言
今天从Books中翻出了“沉淀”已久的关于并发编程书,读完之后,感受颇多,有一些不确定的知识点更加清晰了。在此,结合自己的开发经验,做一个总结。主要从两个方面入手:GCD和Operation,也是目前比较主流的实现并发的方式。
GCD 和 Operation的区别和抉择
GCD是基于libdispatch实现的,目的是为了降低并发的成本;最大的特点是大量使用了block,使多线程的实现变得简单。Operation是基于GCD的封装,更具有封装性,具备GCD不能实现的功能,比如cancel任务。
如何选择?
选用GCD:简单任务,且代码不会被重用;
选用Operation:代码适合封装起来重用;多层异步嵌套(如异步A回调B,B回调C...如果用GCD会造成多层block嵌套,可读性太差);有cancel操作。
不要混淆队列和Thread
先来看一个面试经常遇到的考题:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{ // 这个地方会阻塞主线程
print("hello");
});
首先队列不是线程,队列是一个FIFO的数据结构;创建一个队列,系统会根据系统的使用情况创建一个或者多个线程来配合这个队列工作。线程是实实在在运行逻辑的。你可以抽象成这个样子:
1.png
根据图来分析: 主线程执行主队列中的print("Hello")
操作,而且是同步的,此时主线程等待,主队列任务开始执行,但是主队列的Task需要在主线程完成,这样就出现了两者互相等待的情况,所以造成死锁。所以请一定记住“队列不等于线程”
GCD常见用法
-
创建同步队列
let label = "com.piplab.queue.identifer" let queue = DispatchQueue(label: label)
是队列的唯一标识 ,当队列创建后,OS会选择性的为队列创建或分配一个或多个线程;如果有可复用的线程,就复用线程;否则,将根据情况创建。
主队列比较特殊,App启动时创建,并且是用来处理UI相关操作的同步队列。切记永远不要同步主队列进行一些除UI相关的耗时操作。
-
创建并发队列
let label = "com.piplab.queue.concurrent" let queue = DispatchQueue(label: label, attributes: .current)
除了自定义外,系统已经预定义了6种不同优先级的并发队列,下面会讲到。
-
队列优先级
按照优先级从高到低依次是:
-
.userInteractive
: 一般用于用户交互,需要快速响应的情况。 -
.userInitiated
: 一般用户用户交互之后,需要迅速异步操作的情况,比如用户需要读取数据库,需要快速响应,读取数据。 -
.default
: 系统默认优先级。不要直接使用,否则会出现不必要的错误 -
.utility
:一般用于进度条,IO,网络请求等情况。系统会根据电池情况,平衡响应频率 -
.background
: 一般用户用户不需要感知的情况。 -
.unspecified
: 不建议使用。
这六种优先级并发队列,系统都有预定义。但是并不意味着我们不能创建不同优先级的队列。
let label = "com.piplab.quality" let queue = DispatchQueue(label: label, qos: .userInteractive, attributes: .current)
注意: Queue的优先级并不是一味不变的。如果将一个高优先级的Task交给一个低优先级的Queue,那么Queue的优先级会跟着提升,并且Queue中的其他Task的优先级也会跟着提升
-
-
派发任务
DispatchQueue.global(qos: .utility).async { [weak self] in guard let self = self else { return } // do something // Switch back to the main queue to // update your UI DispatchQueue.main.async { self.textLabel.text = "New articles available!" } }
虽然在block中不声明
[weak self]
也不会造成循环引用,但是会延长self
的声明周期,直到block执行完毕。在切换到主队列时,尽量做比较少的工作。 -
DispatchGroup
适用于“当一组任务完成后,再执行特定的任务,组任务并发执行”。
let group = DispatchGroup() someQueue.async(group: group) { ... your work ... } //任务1 someQueue.async(group: group) { ... more work .... } //任务2 someOtherQueue.async(group: group) { ... other work ... } //任务3 group.notify(queue: DispatchQueue.main) { [weak self] in self?.textLabel.text = "All jobs have completed" //任务4 }
值的注意的是一个group里的Task,可以分配到不同的Queue。 任务4会在任务1,2,3 都执行完之后再执行,如果任务1,2,3中有一直执行不完,那么任务4是不会执行的。
如果不能等待所有的任务执行完,还可以用以下方式实现:
let group = DispatchGroup() someQueue.async(group: group) { ... } someQueue.async(group: group) { ... } someOtherQueue.async(group: group) { ... } if group.wait(timeout: .now() + 60) == .timedOut { print("The jobs didn't finish in 60 seconds") } else { print ("All jobs have complete") }
切记不要在主线程调用
group.wait
方法.对于多层异步嵌套的情况,group还有另外一种实现方式:
queue.dispatch(group: group) { // count is 1 group.enter() // count is 2 someAsyncMethod { defer { group.leave() } // Perform your work here, // count goes back to 1 once complete } }
-
信号量Semaphores
简单的理解信号量 就是一个计数器,每次使用时 -1 ,为0时,代表资源不够,线程等待,释放时 +1 。一个简单的例子: 批量下载图片,最多4个线程同时下载,那么4可以定义为一个信号量。
let semaphore = DispatchSemaphore(value: 4) semaphore.wait() //信号量-1,如果为0,则等待 semaphore.signal() //信号量 +1
-
并发三大难题
- 资源竞争:在多个线程“同时写” 的情况,容易出现错误。
2.png
- 资源竞争:在多个线程“同时写” 的情况,容易出现错误。
假设有一个值value为1,别用两个线程各使value的值+1,正确结果应该是3,但实际效果是:value的值为1,第一个时钟,Thread1读取的值是1;第二个时钟Thread1将value的值修改为2,因为Thread1还没有写回到value中,所以Thread2读取的值是1,第三个时钟Thread1将2写回到value中,thread2将value值+1修改为2,第四个时钟,Thread2将2写回到value中,这样结果为2,所以是错误的。正确的做法是将“+1”操作和写操作同步执行。
`serialQueue.async{ value +1 ,write()}`
下面说一下并发情况下变量访问安全措施(这是另外一个问题,跟上面无关哦)。
当多个线程访问同意变量时,为了避免读的同时写的问题,如图:
3.png
实现如下:
```swift
private let threadSafeCountQueue = DispatchQueue(label: "...", attributes: .concurrent)
private var _count = 0
public var count: Int {
get {
return threadSafeCountQueue.sync {
return _count
}
}
set {
threadSafeCountQueue.async(flags: .barrier) { [unowned self] in
self._count = newValue
}
}
}
```
-
死锁问题:简而言之就是TaskA拥有资源A等待资源B,TaskB 拥有资源B,等待资源A,两者互相等待,谁也拿不到想要的资源。形象点表示,如下图:
4.png
避免这个问题的方法只有一个:每个线程按照统一的顺序申请资源。比如:TaskA需要资源1和资源2,那么TaskB也按照同样的顺序申请资源1和资源2,就不会出现这个问题。
-
优先级反转
这种情况表现为“低优先级的Task拥有高优先级需要的资源,导致高优先级不能执行”。试想一下,一个不着急上厕所的人,拿着所有的纸,即使你再着急,你也没办法啊。造成的原因是“一个低优先级的队列的优先级比高优先级队列的优先级还高”。还记得创建Queue的时候有个优先级的参数吗?
DispatchQueue(..., qos: .userInteractive,...)
,还有分配任务时也有一个优先级参数queue.async(..., qos: .userInteractive,...)``,如果任务的优先级高于队列的优先级,就可能出现这种情况。解决的方法很简单:“任务的优先级不能高于队列”
最后
相信GCD在大家的日常开发中经常用到,一些常见的用法也早已掌握。文中提到的一些注意点是我个人曾经遇到的坎儿,希望对大家有所帮助。如果有不对的地方,欢迎留言指正。不知不觉已经凌晨了,先到这儿,明天写一下Operation。最后悄悄写一句