在不同DispatchQueue操作同一变量的预期行为及线程安全疑问
多DispatchQueue并发写入变量的行为分析与解决方案
一、无同步保护时的预期行为
当多个DispatchQueue(不管是并发队列、串行队列还是当前队列)对同一未加保护的变量执行写入操作时,行为完全未定义。别被i += 1的表象骗了,它本质是三步操作:读取当前值、加1、写回新值。在并发场景下,多个线程可能同时读到同一个旧值,最后写回的结果会重复,导致最终的i远小于预期的40,甚至出现值混乱、重复输出的情况。
二、为什么Playground里看起来是串行的?
Playground的运行环境和真实App不一样:
- 它为了方便调试,会限制并发执行的力度,甚至可能在某些场景下把异步任务串行化执行。
- 你的代码里每个任务只有10次简单操作,执行速度极快,就算并发启动,也可能因为执行太快看起来像是串行完成,但这只是巧合,绝对不能代表真实App的运行情况。
三、必须加同步保护吗?
是的,必须加同步机制保证线程安全。在真实App的并发场景下,不加保护一定会出现数据竞争、值错误的问题。
四、Swift里常用的线程安全方案
1. 用串行队列做同步
专门用一个串行队列处理对i的所有读写操作,确保同一时间只有一个操作访问变量:
import Foundation let syncQueue = DispatchQueue(label: "sync.i.queue") var i = 0 // 写入操作 syncQueue.async { i += 1 print("async 1-1: ", i) } // 读取操作(如果需要) syncQueue.sync { print("当前i的值: ", i) }
或者在原有并发队列里用屏障操作,保证写入时其他操作等待:
let q1 = DispatchQueue(label: "test-1", attributes: .concurrent) var i = 0 // 用屏障包裹写入逻辑,确保写入时队列里的其他任务暂停 q1.async(flags: .barrier) { for _ in 0..<10 { i += 1 print("async 1-1: ", i) } }
2. 使用NSLock手动加锁
通过显式的锁来控制变量的访问权限:
import Foundation let lock = NSLock() var i = 0 q1.async { for _ in 0..<10 { lock.lock() i += 1 print("async 1-1: ", i) lock.unlock() } }
3. 用Swift Actor(Swift 5.5+)
Actor是Swift原生的并发安全类型,自动保证内部状态的线程安全,不需要手动加锁:
import Foundation actor Counter { private var i = 0 func increment() -> Int { i += 1 return i } } let counter = Counter() Task { for _ in 0..<10 { let value = await counter.increment() print("async 1-1: ", value) } }
五、对DispatchQueue的理解纠正
- 并发队列(
.concurrent):会同时调度多个任务执行,任务执行顺序不确定,在多核CPU上是真正的并行执行。 - 串行队列:同一时间只能执行一个任务,任务严格按添加顺序执行。
- 不同队列之间的任务调度是独立的,所以你的代码里q1的两个并发任务、q2的任务、当前队列的同步任务,本来就可能同时执行,Playground的结果只是特殊环境下的巧合,不能当成普遍情况。
内容的提问来源于stack exchange,提问作者hj24
相关产品推荐
相关产品推荐

