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

碎碎念——投资,不确定性沟通定语

碎碎念——投资,不确定性沟通定语

投资理财 最近因为关税的冲击,美股正在经历一波大跌行情。我个人比较看好纳斯达克,也在一直定投纳斯达克。我是长期主义者,没有精力和时间在短期波动中挣钱,只想在下跌调整中「进货」。 定投分左侧定投和右侧定投。左侧定投是在下跌的过程中定投,而右侧定投是在上涨的过程中定投。左侧定投无法确认底部在哪里,需要源源不断往里投入金钱(行内成为「子弹」);右侧定投无法确认反弹是诱多还是形势已经逆转。我采用的是左侧定投,大跌大加,小跌小加,反弹时停止定投。不论采用哪种定投,殊途同归,都是尽量降低投资成本。 目前网上看衰美股的声音不少,不少人因为恐慌割肉卖出股票。但我们要知道目前美国仍旧是世界第一大国,消费潜力巨大,大型科技公司(苹果、英伟达等)的基本面并没有出现大问题。只是因为特朗普的「量子态」关税政策,导致市场恐慌抛售。我们无需担心纳斯达克、标普指数从此一蹶不振。恰恰相反,现在是买入美股的绝佳时机。苹果、英伟达等大型公司的 PE 值已经降到了合理位置,只要不买妖股,不投机,只关注纳斯达克、标普指数,只买大型公司股票,迟早会取得丰厚盈利的。

By Gray
怀念小时候吃过的食物

怀念小时候吃过的食物

前两天下班骑车回家的路上听到了路旁有人在讨论泡馍。他们口中的泡馍应该是类似西安羊肉泡馍之类的食物。但是我却想起来了小时候吃的不一样的泡馍以及其他吃食。 不一样的泡馍 小时候我们那里普遍比较贫穷,家家户户除了过年过节基本上很难吃到大块肉。小孩子饭量时小时大,中午吃的饭,半晌就又饿了。家里有大葱或者豆糁的话,可以拿着一个馍就着就吃了。整根的葱是最下馍的,葱白部分甜又辣,葱叶里面会有像鼻涕一样的粘液,要把它挤出来才下得嘴吃。豆糁是黄豆的发酵产物,煮熟的大豆加盐发酵几天,黏丝丝的时候团成球,放到发黑就能吃了。吃的时候从球上掰下来几小块就行。豆糁是咸的,因而也能下饭。不过最妙的吃法是将豆糁和鸡蛋一起炒。鸡蛋的香气和豆糁稍微发臭的味道混在一起,形成一种独特的香味。像北京的臭豆腐一样,闻着臭,吃着香。 如果家里没葱没豆糁了,馍又很干,那泡馍就是解决饿肚子的绝好办法。将干硬的馍掰成几瓣,不能太碎小,放到瓷碗里。倒入炒菜的肉味王佐料,或者是平时攒下来的方便面调料。再提溜着暖水瓶,倒进去冒着热气的水。当然香油是少不了的,拿着油光光的瓶子,滴进去几滴喷香的香油。最后用大碗盖住,或者干脆啥也不盖,静等

By Gray
Swift Server Push Notification 配置

Swift Server Push Notification 配置

获取证书 在 Apple Developer 开发者账号 Certificates, Identifiers & Profiles 里选择 Keys。新增一个 key, configure 里选择 Sandbox & Production。下载该 p8 证书,并且保存好(只能下载一次)。 终端 cd 到证书所在路径,输入下面指令。 openssl pkcs8 -nocrypt -in AuthKey_XXXXXXXXX.p8 -out ~/Downloads/key.pem cat key.pem 得到 PRIVATE KEY 字符串,复制好。 服务端配置 服务端有多种技术栈方案,包括 Java、

By Gray
香港游记——一个传统而又现代的城市

香港游记——一个传统而又现代的城市

这是 2024 年的最后一场旅行,从北京到香港,跨越了大半个中国。去香港,一方面是想领略一下它的文化和风光,另一方面是想办一个香港银行卡,买港美股以及海外收付款。 从北京到香港,动卧是一个不错的选择。乘坐 D903 次动车,晚上八点登车,睡一觉,第二天一早就到深圳北了。再从深圳北坐高铁过口岸到香港西九龙,差不多上午九点多就能到达香港。深圳北到西九龙的高铁车次非常多,不用担心买不到票。 密集的建筑 香港给我的初印象就是——这里的楼房真的很密集。不光是住宅区又高又密,商业区的建筑物与建筑物之间也几乎只有街道相隔,很少见到大型的公园或者绿化带。土地利用率很高。这一点和北京差别还是挺大的。北京虽然也是寸土寸金,但是市内绿化面积很高,大型公园也很常见。 街上密集的建筑,让人第一眼看就知道这是香港。 旧与新,传统与现代 在香港,不同地区的风格面貌会相差很多。你既能见到破旧不堪、需要修缮的古老楼房,也能见到银光闪闪、科技感十足的现代化大厦。这种新与旧的切换,传统和现代的反差,总是能给人强烈的震撼。这正是香港的魅力所在。 维多利亚港和中环摩天轮 维多利亚港是香港的中心,是香港旅游

By Gray