并发队列中DispatchSourceTimer事件与取消回调同步性及线程安全问询
核心结论
- 你观察到单次定时器触发事件后仍执行取消回调是符合libdispatch设计的:官方文档描述的
cancel()会「阻止事件handler后续调用」,指的是阻止还未提交到队列的新事件,已经触发提交、正在队列中等待/已经开始执行的事件handler不会被中断,且取消handler无论事件是否触发都会最终调度执行。 - 你不能假设
cancel()调用和事件handler在业务层面是互斥同步的:libdispatch内部的同步仅保证dispatch source的状态不会出现异常,不会序列化已经提交的两个handler的执行,默认使用全局并发队列的情况下,事件handler和取消handler完全可能在不同线程并行执行。 - 你当前的
DispatchWorkItem写法存在竞态条件,线程不安全:DispatchWorkItem.cancel()仅会标记任务为取消状态,不会中断已经开始执行的任务,会出现取消handler已经启动执行,事件handler才调用cancel()的情况,最终两个回调逻辑都会执行。
安全实现方案
最简单可靠的方案是给定时器指定自定义串行队列作为目标队列,所有回调都会被序列化到该队列执行,天然避免并发竞态,通过布尔状态位即可保证两个回调仅执行一个:
// 自定义串行队列,所有timer回调都派发到该队列执行,保证串行无并发 let timerQueue = DispatchQueue(label: "com.example.timer.serialQueue") let timer = DispatchSource.makeTimerSource(queue: timerQueue) // 状态位仅在timerQueue上读写,无需额外加锁 var hasExecuted = false timer.setEventHandler { guard !hasExecuted else { return } hasExecuted = true // 事件业务逻辑 print("event handler") } timer.setCancelHandler { guard !hasExecuted else { return } hasExecuted = true // 取消业务逻辑 print("cancel handler") } timer.schedule(wallDeadline: .now()) timer.activate() DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { timer.cancel() }
如果要继续使用DispatchWorkItem方案,需要同时满足两个条件:
- 给timer指定串行队列作为目标队列,序列化两个回调的执行
- 在取消任务开头判断取消状态,避免已经被标记取消的任务继续执行
let timerQueue = DispatchQueue(label: "com.example.timer.serialQueue") let timer = DispatchSource.makeTimerSource(queue: timerQueue) let cancelHandler = DispatchWorkItem { guard !cancelHandler.isCancelled else { return } print("cancel handler") } timer.setEventHandler { cancelHandler.cancel() print("event handler") } timer.setCancelHandler(handler: cancelHandler) timer.schedule(wallDeadline: .now()) timer.activate() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { timer.cancel() }
内容的提问来源于stack exchange,提问作者pronebird
相关产品推荐
相关产品推荐

