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

《漫步华尔街(第12版)》读书笔记

《漫步华尔街(第12版)》读书笔记

股票分析 基本面分析 * 基本面分析的四个基本决定因素 * 预期增长率 * 复合增长(复利)对投资决策有很重要的意义。 * 一只股票的股利增长和盈利增长率越高,理性投资者应愿意为其支付越高的价格。 * 推论:一只股票的超常增长率持续时间越长,理性投资者应愿意为其支付越高的价格。 * 预期股利支付率 * 对于预期增长率相同的两只股票来说,持有股利支付率越高的股票,较之股利支付率低的股票,会使你的财务状况更好。 * 在其他条件相同的情况下,一家公司发放的现金股利占其盈利的比例越高,理性投资者应愿意为其股票支付越高的价格。 * 特例,很多处于强劲增长阶段的公司,往往不支付任何股利。这时候不满足「在其他条件相同的情况下」。 * 风险程度 * 在其他条件相同的情况下,一家公司的股票风险越低,理性投资者(以及厌恶风险的投资者)应愿意为其股票支付越高的价格。 * 市场利率水平 * 在其他条件相同的情况下,市场利率越低,理性投资者应愿意为股票支付越高的价格。 * 举例,银行存款利率

By Gray
2025 端午日本九日游

2025 端午日本九日游

从日本回来后就一直忙个不停,忙着搬家和工作。这周末终于有时间回顾和记录一下日本的旅游行程。 这次出国游是年初就规划好的。端午节假期三天再加上节后请假四天,以及周末,总共能休 9 天。5 月 31 号出发,6 月 9 号凌晨的航班飞回北京。 出发前的准备 机票和酒店 越临近出发日期,机票和酒店就越贵。所以我们早早地就把机票和酒店定了。 去程机票订的山航,青岛转机,5 月 31 号从北京出发抵达青岛,在青岛玩一天,翌日早上从青岛飞往关西机场。回程机票订的海南航空,从东京羽田机场直飞北京,是凌晨两三点的红眼航班。 本次行程要去关西(京都、大阪、奈良)、关东(东京、富士山)。关西三个城市很近,一直住在京都即可,从京都往返大阪和奈良。关东就住在东京。京都的酒店订在了京都站附近,出站走几步就能到,交通非常便利。东京的酒店订在了马喰町附近,附近有很多地铁线路,包括浅草线、

By Gray
2025 关税危机中学到的投资经验

2025 关税危机中学到的投资经验

充足的现金流很重要 好的买入机会不会每天都出现,但当它出现的时候,你最好还有筹码可以投入。 有些人手里握不住钱,一有闲钱就赶紧买入基金、股票,生怕错过了机会,让钱白搭手里。市场是疯狂的、充满变数的,尤其是在特朗普上台后,一句话就可能让股市涨停或跌停。那些专业的理财投资机构尚不能预测市场,何况我们这些散户呢。在不稳定的市场中,我们要学习巴菲特,备好现金,耐心等待买入(抄底)机会。 不要提前打光子弹 美股标普 500 指数从 2 月中旬到 3 月中旬累计跌了约 10%。如果这时候你觉得已经跌了很多,可以 all in 抄底了,那么你就会错过 4 月上旬的那次狂跌——一周跌了约 10%。没有人能预测市场,除了此刻的股市指挥家特朗普。散户们能学到的经验就是「永远不要提前打光子弹」,你以为的谷底其实只是个半山腰。 相信自己,保持耐心 在美股大跌的时段里,小红书、v2ex

By Gray