iOS开发中的多线程操作是开发的基本技术之一,iOS有四种多线程编程的技术,分别是:NSThread,Cocoa NSOperation,GCD(全称:Grand Central Dispatch), pthread。今天我们就重点讲一讲 GCD 中的并发,锁和线程同步。
GCD中的并发
GCD 队列默认就是串行的(serial),在 GCD 中创建并发队列是如下所示:
1 | let queue = DispatchQueue(label: "current", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.never, target: nil) |
你也可以直接创建:
1 | let queue = DispatchQueue.init(label: "concurrent", attributes: .concurrent) |
label: 队列的标识符,能方便区分队列并调试。
qos: 队列的优先级,大多情况下用默认即可。
attributes: 队列的属性,默认是串行的,concurrent是并发的,initiallyInactive表示队列不会自动执行,需要开发中调用activate()
手动触发,对应的还有suspend()
和 resume()
。
activate()
:开始执行队列suspend()
:挂起队列resume()
:继续执行队列
autoreleaseFrequency:自动释放频率,有些列队会在执行完任务之后自动释放,有些是不会自动释放的,需要手动释放。
线程安全:锁
在并发中,最重要的就是如何保证线程的安全。这就涉及到一个重要的知识点:锁。在 Objective-C
加锁的常见方式为 @synchronized
关键词和 NSLock
对象锁。Swift 的 GCD 中我们可以使用信号量 DispatchSemaphore
的方式实现加锁的目的。
我们先来说说信号量加锁的方式
DispatchSemaphore
DispatchSemaphore
提供了传统计数信号量的高效实现,可用于控制跨多个执行上下文访问资源。
举个例子:线程 A 执行的前提是需要线程 B 执行的结果,但是 A,B 是两个异步线程。简单的来说就是如何串行的执行两个异步线程。
1 | let sema = DispatchSemaphore(value: 0) |
打印输出为 2,1
上面的代码如果不用信号量处理,输出的结果为 1 2
,wait()
就是阻塞当前队列,signal()
发出信号。DispatchSemaphore
的 value
参数表示初始的信号量,不要设置成负数,否则会抛出 EXC_BAD_INSTRUCTION
异常。另一个就是要保证 wait()
和 signal()
的平衡,也就是成对的出现。
- 当一个信号量被通知
signal()
,计数会加1; - 如果一个线程等待一个信号量
wait()
,线程会被阻塞,直到计数器>0,此时开始运行,并且对信号量减1。
所以先运行到sema.wait()的时候,当前计数器为0,所以阻塞,然后在sema.signal()后,计数器变成1了,此时第一个线程可以执行了。
线程的同步
1. enter() - leave() - notify()
线程的同步我们来介绍一下 GCD 中的 DispatchGroup
。线程同步也可以用信号量的方式来实现,这里就不在啰嗦。
当我们想要在并发结束后再同步执行东西。首先我们声明一个 DispatchGroup
:
1 | let group = DispatchGroup() |
我们需要用到三个方法:enter()
,leave()
,notify(...)
。
和queue.async相比,当我们调用n次enter()后再调用n次leave()时,notify和wait会收到同步信号;这个特点使得它非常适合处理异步任务的同步,当异步任务开始前调用enter()**异步任务结束后调用leave()**;
1 | let queue = DispatchQueue(label: "concurrent", attributes: .concurrent) |
控制台打印如下:
1 | 323 |
我们再把任务二的enter和leave去掉,代码如下:
1 | let queue = DispatchQueue(label: "concurrent", attributes: .concurrent) |
控制台打印如下:
1 | 323 |
说明group的enter和leave出现的时候,会触发group的notify。
2.DispatchGroup - notify()
直接用队列方法里的group也可以达到上面enter()和leave()的例子,但前提是闭包里执行的得是同步任务才可以,代码如下:
1 | let group = DispatchGroup.init() |
控制台打印如下:
1 | 2323 |
如果闭包里变成异步任务,如下:
1 | let group = DispatchGroup.init() |
控制台打印如下:
1 | 2323 |
总结1:可以发现会直接触发notify方法,而不会等待异步任务完成后。但是enter()和leave()就不会有这个问题。有兴趣的话自己再写一下,这里就不贴代码了。
其他:
1.barrier
barrier栅栏函数的作用是将barrier之前的闭包和barrier之后的闭包隔绝开来,如下代码
1 | let queue = DispatchQueue.init(label: "concurrent", attributes: .concurrent) |
控制台输出:
1 | 最后的任务 |
2.After
1 | print("zxcv") |
输出:
1 | zxcv |
总结2:GCD中最起码有三种方式可以实现多个异步操作后再执行同步的操作,分别是Semaphore锁,group,barrier,其中group又具体分enter()、leave()和队列调用group两种方式,不过后者不支持异步队列中包含异步任务,需要配合锁来使用。