以前认为信号量的初始值就是线程的最大并发数,不可更改的,其实并不然。
平时开发一般都使用GCD信号量(DispatchSemaphore)来解决线程安全问题:当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
经典的多线程安全隐患示例 - 卖票
- 使用信号量之前:
var ticketTotal = 15
let group: DispatchGroup = DispatchGroup()
// 卖票操作
func __saleTicket(_ saleCount: Int) {
DispatchQueue.global().async(group: group, qos: .default, flags: []) {
for _ in 0..<saleCount {
// 加个延时可以大概率让多条线程同时进行到这一步
sleep(1)
// 卖一张
self.ticketTotal -= 1
}
}
}
// 开始卖票
func startSaleTicket() {
print("\(Date()) 一开始总共有\(ticketTotal)张")
print("\(Date()) 第一次卖5张票")
__saleTicket(5)
print("\(Date()) 第二次卖5张票")
__saleTicket(5)
print("\(Date()) 第三次卖5张票")
__saleTicket(5)
group.notify(queue: .main) {
print("\(Date()) 理论上全部卖完了,实际上剩\(self.ticketTotal)张")
}
}
打印结果:
- 使用信号量对卖票的操作进行加解🔐:
func __saleTicket(_ saleCount: Int) {
DispatchQueue.global().async(group: group, qos: .default, flags: []) {
for _ in 0..<saleCount {
// 加个延时可以大概率让多条线程同时进行到这一步
sleep(1)
// 卖一张
self.semaphore.wait() // 加🔐
self.ticketTotal -= 1
self.semaphore.signal() // 解🔐
}
}
}
打印结果:
对信号量的误解
以前认为信号量的初始值是指线程的最大并发数,不可更改的,直到看到其他文章介绍的一个信号量用法:
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
func semaphoreTest() {
DispatchQueue.global().async {
DispatchQueue.main.async {
// 从主队列中获取一些信息
...
// 发送信号
self.semaphore.signal()
}
// 开始等待
self.semaphore.wait()
// 等待结束,线程继续
}
}
看到这个用法就开始觉得奇怪了,明明初始化为0,不就是线程最大并发数为0吗?不就是不能有线程可以工作吗?按道理应该会一直阻塞住这个子线程才对,那这种用法有什么意义呢?
对信号量的重新认识
众所周知,semaphore.wait()
是减1操作,不过这个减1操作的前提是信号量是否大于0:
- 如果大于0,线程可以继续往下跑,然后紧接在
semaphore.wait()
这句过后,才会真正对信号量减1; - 如果等于0,就会让线程休眠,加入到一个都等待这个信号的线程队列当中,当信号量大于0时,就会唤醒这个等待队列中靠前的线程,继续线程后面代码且对信号量减1,也就确保了信号量大于0才减1,所以不存在信号量小于0的情况(除非在初始化时设置为负数,不过这样做的话当使用时程序就会崩溃)。
而semaphore.signal()
是对信号量的加1操作,后来经过测试发现,通过semaphore.signal()
可以任意添加信号量,所以初始化的信号量并非不可更改的,是可以随意更改的。
- 验证的🌰:
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // 0
func semaphoreTest() {
semaphore.signal() // 0 + 1 = 1
semaphore.signal() // 1 + 1 = 2
semaphore.signal() // 2 + 1 = 3
semaphore.wait() // 3 - 1 = 2
print("\(Date()) \(Thread.current) hello_1")
semaphore.wait() // 2 - 1 = 1
print("\(Date()) \(Thread.current) hello_2")
semaphore.wait() // 1 - 1 = 0
print("\(Date()) \(Thread.current) hello_2")
// 延迟3秒去另一个线程异步添加信号量
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
print("\(Date()) \(Thread.current) 信号量+1")
let result = self.semaphore.signal() // 0 + 1 = 1
print("\(Date()) \(Thread.current) result: \(result)");
/*
* PS: signal() 会返回一个结果,文档解释为:
* This function returns non-zero if a thread is woken. Otherwise, zero is returned. 如果线程被唤醒,则此函数返回非零。否则,返回零。
* 这里执行后会有一条线程被唤醒,所以返回1,前面的3次signal()返回的都是0,说明没有线程被唤醒,不过信号量的确是有+1的。
*/
}
semaphore.wait() // 等于0就”卡住“当前线程
print("\(Date()) \(Thread.current) hello_4") // 1 - 1 = 0
}
打印结果:
证明了信号量是可以自己维护的,只是“看不见”(没有API获取)。
- 伪代码解释前面介绍的用法:
// 初始化信号量为0,假设 semaphoreCount 是代表信号量的一个数字
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // semaphoreCount = 0
func semaphoreTest() {
DispatchQueue.global().async {
//【1】开始执行任务1
DispatchQueue.main.async {
//【4】开始执行任务2
...
//【5】任务2结束,信号量加1,发送信号,唤醒等待靠前的线程
self.semaphore.signal() // semaphoreCount + 1 = 1
}
//【2】任务1需要等待任务2执行完才继续,判断有无信号量
self.semaphore.wait() //【3】判断信号量,发现 semaphoreCount == 0,这里”卡住“(休眠)
//【6】能来到这里,说明信号量至少为1,唤醒了这条线程,同时对信号量减1
// semaphoreCount - 1 = 0
// 减1后如果等于0,那么其他还在等这个信号量的线程只能继续等,而这条线程会继续往下执行。
//【7】任务1继续
}
}
总结
- GCD信号量的初始值的确是线程的最大并发数,不过这个并发数不是不能修改的,可以通过
semaphore.signal()
任意添加的,相当于是有个隐藏的semaphoreCount
来控制能有多少条线程能同时工作; - 这个
semaphoreCount
至少要有 1 才可以执行代码,只要是 0,semaphore.wait()
就会让线程休眠等着直到semaphoreCount
大于 0 才唤醒; - 由于没有API获取这个
semaphoreCount
,所以一定要注意:用过多少次semaphore.wait()
就记得也要用多少次semaphore.signal()
,保证使用配对,不然线程会永远休眠。
知道这些后,以后GCD信号量除了可以加解🔐外,也可以做到让当前线程等待别的线程了,也就是说可以控制线程的执行时机喔~
GCD其它一些需要自己维护配对次数的函数
这些函数也是没有相应API获取次数,需要自己维护:
- 队列组:
group.enter()
和group.leave()
- 定时器:
timer.resume()
和timer.suspend()
- PS:不要在暂停
suspend
状态下执行cancel()
,否则会崩溃,所以记得在cancel()
前确定 timer 是在运行resume
状态下。
- PS:不要在暂停
网友评论