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 配合使用,完成一些比较复杂的逻辑流程。

Read more

2025 年度总结

2025 年度总结

今天是 2026 年 1 月 1 号,又是新的一年。这个元旦没有安排出行任务,就在家里休息休息,或者出门溜达溜达。昨天休了一天全薪病假,做了体检,写了年终绩效总结,晚上干了一顿烤肉,没有时间写个人的年度总结。今天起早写写总结。 以下「今年」指 2025 年。 职业发展 算起来,我已经毕业工作四年多了。职业发展整体上还算稳定,没有碰到过糟心事,遇到的领导们也都对我关怀有加。今年又晋升一次,薪资迈上新的台阶。越往上升,越觉得离职业生涯的终点越近,逼迫自己赶紧找个靠谱稳定的副业,到 35 岁没人要的时候能养活自己。 最近两年 AI 大模型的崛起,提高了许多行业的可替代性。码农虽然不是首当其冲的,但危机感已经弥漫在各个论坛博客公共平台上面。没有人能准确预测到未来发展,但做好两手准备是很有必要的。码农不能再只低着头守着自己的键盘和屏幕,也要往外看,接触社会上的各种信息,打破信息壁垒。掌握的信息越多,出路就越多。

By Gray
联通 FTTR 宽带从路由器设置自动重启和穿墙功率

联通 FTTR 宽带从路由器设置自动重启和穿墙功率

几个月前把家里宽带换成了联通的千兆 FTTR 宽带,包含一主一从两个点位。配套光猫设备是华为的星光 F50 尊享版。 主点位放置在客厅茶几上,方便连接电视。从点位放在卧室门口,那里恰好有一个不耽误过路的小拐角可以放路由器。平常我们基本不在客厅活动,其他区域最近的 Wi-Fi 信号源是从路由器,因此我们大多数的设备连接的都是从路由器。从路由器的工作负荷很大。 从路由器个头小主路由器很多,散热不咋地。工作时间久了发热就容易发生数据包堵塞,丢包延迟高。需要把它电源拔掉重启。从宽带开通到现在,数据包堵塞影响网络的情况每个月会发生一次。有一次还影响了居家办公的视频会议。宽带维修师傅也给不出有效的法子,建议就是定期插拔从路由器电源。 从路由器和书房之间隔了两堵墙。信号到我书桌那个位置时,千兆网速已经衰减到只有 400-500Mbps 了,折损将近一半。叠加路由器发热的 debuff,书桌位置的网速最差的时候几乎和百兆宽带差不多。 我尝试过在光猫后台管理将路由器功率设置到「穿墙」模式,但没有任何作用。今天在后台研究了一番发现,原来我之前设置的功率是仅对主路由器生效,从路由器还是标准功率。要修

By Gray