Dispatch Semaphore VS Dispatch Group

Dispatch Group

Dispatch Group 在项目中比较常见,用于多任务多线程之间的协作。比如,使用两个队列分别请求不同的接口,等请求全部完成后,刷新页面。下面这段代码使用两个队列分别执行不同任务,当两个任务都完成后,通知主队列执行完成代码

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let group = DispatchGroup()
let q1 = DispatchQueue(label: "q1")
group.notify(queue: .main) {
    print("Task All Done!")
}
q1.async(group: group) {
    for i in 1 ... 5 {
        print("q1 >>> \(i)")
    }
}
let q2 = DispatchQueue(label: "q2")
q2.async(group: group) {
    for i in 1 ... 5 {
        print("q2 >>> \(i)")
    }
}

// 输出
q1 >>> 1
q2 >>> 1
q1 >>> 2
q2 >>> 2
q2 >>> 3
q1 >>> 3
q2 >>> 4
q2 >>> 5
q1 >>> 4
q1 >>> 5
Task All Done!

需要注意的是,网络请求一般是异步的。所以不能 queue.async(group: group) {} 就完事了,而是要使用 Dispatch Groupenterleave() 方法。如下:

group.enter()
request.start { request in
    complete(true)
    group.leave()
} failure: { request in
    complete(false)
    group.leave()
}

Dispatch Semaphore

简介

Dispatch Semaphore 像是一个闸机,每次发出一个信号(刷一次卡)就放过一段代码。比如下面这段代码,

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

let semaphore = DispatchSemaphore(value: 0)
let serialQueue = DispatchQueue(label: "serial")

print("Task Start!")

serialQueue.async {
    print("pause")
    semaphore.wait()
    print("continue step 1")
    semaphore.wait()
    print("continue step 2")
}

for i in 1 ... 2 {
    DispatchQueue.global().asyncAfter(deadline: .now() + Double(i)) {
        print(">>> step \(i) continued")
        semaphore.signal()
    }
}

// 输出
Task Start!
pause
>>> step 1 continued
continue step 1
>>> step 2 continued
continue step 2

每次发出一个信号 signal(),就放开最前面的一处等待 wait()Dispatch Semaphore 具备类似 NSLock 的特性,可用于加锁解锁、线程同步等场景,而且性能很高,是比较推荐的一种同步方式。

初始化参数 value 的作用(重要)

初始化方法 DispatchSemaphore(value: 0) 里面有个 value 参数,它的含义非常重要。官方文档解释如下:

Passing zero for the value is useful for when two threads need to reconcile the completion of a particular event. Passing a value greater than zero is useful for managing a finite pool of resources, where the pool size is equal to the value.

意思是,如果要协调多线程特定事件的完成时,比如重要用法的多线程请求网络,把 value 设置为 0;如果要处理一串有限的任务,而且要保证处理顺序是特定的或者线程安全的(同步锁),就 value 设置为一个大于 0 的整数。

value 是 0 时,Dispatch Semaphore 的所有 wait() 不会在运行时被自动放过,只能在得到一个 signal() 时,解锁一个 wait();而当 value > 0 时(假设为 x ),Dispatch Semaphore 的前 x 个 wait() 不用得到 signal() 就能被放过。通俗的讲 value 就是在初始化的时候就给它 x 个通行证。比如下面这段代码,

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

let dispatchQueue = DispatchQueue(label: "com.liang.playground", attributes: .concurrent)

let semaphore = DispatchSemaphore(value: 0)

dispatchQueue.async {
    semaphore.wait()
    Thread.sleep(forTimeInterval: 4)
    print("Sema block 1")
    semaphore.signal()
}

dispatchQueue.async {
    semaphore.wait()
    Thread.sleep(forTimeInterval: 2)
    print("Sema block 2")
    semaphore.signal()
}

dispatchQueue.async {
    semaphore.wait()
    print("Sema block 3")
    semaphore.signal()
}

dispatchQueue.async {
    semaphore.wait()
    print("Sema block 4")
    semaphore.signal()
}
  • 如果初始化的 value 给的是 0,那么将不会有任何输出。因为所有的 wait() 都需要 signal() 来解锁,但是初始化的时候是没有给它一个通行证的
  • 如果初始化的 value 给的是 1,那么输出如下:
Sema block 1
Sema block 2
Sema block 3
Sema block 4

这是因为在初始化的时候给了程序一个通行证,所以第一个 wait() 便能顺利通过了。等待 4 秒后,会发出一个 signal(),解锁第二个 wait() …所有的异步都能顺利的执行了

  • 如果初始化的 value 给的是 2,那么输出如下:
Sema block 2
Sema block 3
Sema block 4
Sema block 1

这是因为在初始化的时候给了程序连个通行证,这样前两个 wait() 就形同虚设了。但是第二个 block 会更快执行完,所以它先被打印出来。

重要用法

保证资源的线程安全

利用 Dispatch Semaphore 能够设置初始通行证数量(value)的能力,可以保证一个资源在多线程操作时是线程安全的。

比如以下代码保证了变量 num 在被多个线程操作时,是线程安全的

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

let sema = DispatchSemaphore(value: 1)
var num = 0
func addNum() {
    sema.wait()
    num += 1
    print(">>> num = \(num)")
    sema.signal()
}

DispatchQueue.global().async {
    for i in 1 ... 20 {
        addNum()
    }
}

DispatchQueue.global().async {
    for i in 1 ... 20 {
        addNum()
    }
}

// 输出
>>> num = 1
>>> num = 2
>>> num = 3
>>> num = 4
>>> num = 5
>>> num = 6
>>> num = 7
>>> num = 8
>>> num = 9
>>> num = 10
>>> num = 11
>>> num = 12
>>> num = 13
>>> num = 14
>>> num = 15
>>> num = 16
>>> num = 17
>>> num = 18
>>> num = 19
>>> num = 20
>>> num = 21
>>> num = 22
>>> num = 23
>>> num = 24
>>> num = 25
>>> num = 26
>>> num = 27
>>> num = 28
>>> num = 29
>>> num = 30
>>> num = 31
>>> num = 32
>>> num = 33
>>> num = 34
>>> num = 35
>>> num = 36
>>> num = 37
>>> num = 38
>>> num = 39
>>> num = 40

多线程网络请求

利用 Dispatch Semaphore 也能实现多个请求完成后通知主线程刷新 UI 的功能,代码像下面这样:

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

let semaphore = DispatchSemaphore(value: 0)
let serialQueue = DispatchQueue(label: "serial")

serialQueue.async {
	// 3 requests
    semaphore.wait()
    semaphore.wait()
    semaphore.wait()
    DispatchQueue.main.async {
        print(">>> update UI in main thead")
    }
}

// request 1
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    print(">>> request 1 finished")
    semaphore.signal()
}
// request 2
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    print(">>> request 2 finished")
    semaphore.signal()
}
// request 3
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
    print(">>> request 3 finished")
    semaphore.signal()
}

// 输出
>>> request 1 finished
>>> request 2 finished
>>> request 3 finished
>>> update UI in main thead

总结

Dispatch Semaphore 通常用于单个事件的处理,而 Dispatch Group 通常用于多个事件的处理。

Dispatch Semaphore 更加简朴,性能上要优于 Dispatch Group,我们应在代码中充分发挥利用它的特性,或者与 Dispatch Group 配合使用,完成一些比较复杂的逻辑流程。