iOS 并发,锁,线程同步【一】GCD

无并发,不编程。并发在开发中是非常重要的一个技术,运用并发技术,可以写出高性能的程序,并发能够有效地利用多核心 CPU 的优势来提高数据处理的速度。作为一个码农,学好并发是十分有必要的。iOS有四种多线程编程的技术,分别是:NSThread,Cocoa NSOperation,GCD(全称:Grand Central Dispatch), pthread。今天我们就重点讲一讲 GCD 中的并发,锁和线程同步。

GCD 中的并发


GCD 队列默认就是串行的(serial),在 GCD 中创建并发队列是如下所示:

1
let concurrent = DispatchQueue(label: "com.demo.concurrentQueue", attributes: .concurrent)

DispatchQueueattributes 参数还有一个取值:initiallyInactive,这是可以手动管理队列执行时间的参数。
当一个队列声明为 initiallyInactive 时,这个队列不会自动开始执行,必须要调用 activate() 方法。对应的还有 suspend()resume()

  • activate():开始执行队列
  • suspend():挂起队列
  • resume():继续执行队列

关于 initiallyInactive 到这里为止,我们继续说说并发队列。

线程安全:锁


在并发中,最重要的就是如何保证线程的安全。这就涉及到一个重要的知识点:锁。在 Objective-C 加锁的常见方式为 @synchronized 关键词和 NSLock 对象锁。Swift 的 GCD 中我们可以使用信号量 DispatchSemaphore 的方式实现加锁的目的。

我们先来说说信号量加锁的方式。

DispatchSemaphore

DispatchSemaphore 提供了传统计数信号量的高效实现,可用于控制跨多个执行上下文访问资源。

举个例子:线程 A 执行的前提是需要线程 B 执行的结果,但是 A,B 是两个异步线程。简单的来说就是如何串行的执行两个异步线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
let sema = DispatchSemaphore(value: 0)

DispatchQueue.global().async {
sema.wait()
print("1")
}
DispatchQueue.global().async {
sleep(2)
print("2")
sema.signal()
}

// 2 1

上面的代码如果不用信号量处理,输出的结果为 1 2wait() 就是阻塞当前队列,signal() 发出信号。DispatchSemaphorevalue 参数表示初始的信号量,不要设置成负数,否则会抛出 EXC_BAD_INSTRUCTION 异常。另一个就是要保证 wait()signal() 的平衡,也就是成对的出现。

简单的介绍了一下 DispatchSemaphore,现在我们用它来实现我们的锁。

思考一下锁是为什么会存在?锁就是为了解决不同线程之间同时的访问同一数据可能会造成意想不到的错误而存在。那么我们用 DispatchSemaphore 实现的时候,就是要保证当线程 A 访问数据的时候我们需要阻塞下一个线程 B 对这一块数据的访问,当 A 完成对数据的访问时,我们才能允许线程 B 对这一块数据的访问。理清思路,下面我们就来转换成代码表示:

我们可以将锁的逻辑封装在一个方法中,充分利用 Swift 函数式编程的优点:

1
2
3
4
5
func synchronized(_ closure: () -> ()) {
sema.wait()
closure()
sema.signal()
}

在声明 DispatchSemaphore 的时候,它的初始信号量 value 就不能是0了,必须是1(原因很简单)。所以我们必须这样声明:

1
let sema = DispatchSemaphore(value: 1)

OK,锁“做”好了,我们就开始我们的并发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

func task(_ idx: Int) {

print("Start sync task \(idx)")
synchronized() {
ary.append(idx)
}
sleep(2)
print("End sync task \(idx)")
}

print("Main queue Start")
let concurrent = DispatchQueue(label: "com.demo.concurrentQueue",
attributes: .concurrent)
for idx in 0..<3 {
concurrent.async {
task(idx)
}
}
print("Main queue End")

这样我们就能保证我们 ary 中的数据时正确的。

Swift 中的 @synchronized

刚才我们介绍了信号量编写的锁,接下来我们来看看 Objective-C 中的 @synchronized,是的,Swift 中没有 @synchronized 这个东西,怎么办呢?其实 @synchronized 底层调用的是 objc_sync_enter(_ obj: Any)objc_sync_exit(_ obj: Any)。我们就直接调用这两个方法就 OK。

1
2
3
4
5
func synchronized(_ lock: Any, _ closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}

这里的 lock 参数表示要加锁的对象。

1
2
3
synchronized(ary) {
ary.append(idx)
}

顺便一提: @synchronized 是互斥锁,由于内部会进行异常处理,Objective-C 中性能一般。我们实现的 DispatchSemaphore 信号量锁,由于底层是 C 代码的封装,所以性能上要好点。

详细的解释可以参考我的另一篇文章:Swift Lock

线程的同步


线程的同步我们来介绍一下 GCD 中的 DispatchGroup。线程同步也可以用信号量的方式来实现,这里就不在啰嗦。

当我们想要在并发结束后输出 ary 中的数据的时候,我们就需要线程的同步了。首先我们声明一个 DispatchGroup

1
let group = DispatchGroup()

我们需要用到三个方法:enter()leave()notify(...)

  • enter() 表示执行的开始
  • leave() 表示执行的结束
  • notify(...) 所有执行都结束后执行的函数

我们执行的开始就是我们的并发函数:

1
2
3
4
concurrent.async {
group.enter()
task(idx)
}

执行的结束就是我们的task:

1
2
3
4
5
6
7
8
9
10
func task(_ idx: Int) {

print("Start sync task \(idx)")
synchronized(ary) {
ary.append(idx)
}
sleep(2)
print("End sync task \(idx)")
group.leave()
}

最后我们执行输出 ary 信息的方法:

1
2
3
group.notify(queue: .main) {
print(ary)
}