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

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

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

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

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

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

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

By Gray