0

默认情况下,在创建 时DispatchSourceTimer,默认并发队列用于调度定时器事件和取消。

有趣的是,即使在事件处理程序已经触发之后,单次计时器仍然会调度对取消处理程序的调用。

所以考虑下面的代码:

let timer = DispatchSource.makeTimerSource()

timer.setEventHandler {
    print("event handler")
}

timer.setCancelHandler {
    print("cancel handler")
}

timer.schedule(wallDeadline: .now())
timer.activate()

DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
    timer.cancel()
}

输出:

event handler
cancel handler

现在根据文档,调用cancel()应该阻止事件句柄执行,但是假设调用cancel()与内部事件处理程序的调用同步是否安全?

异步取消调度源,防止对其事件处理程序块的任何进一步调用。

我想确保调用其中一个或另一个,但不能同时调用两者,所以我修改了我的代码并将取消处理程序包装到DispatchWorkItem,我从事件处理程序内部取消:

let timer = DispatchSource.makeTimerSource()

var cancelHandler = DispatchWorkItem {
    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()
}

但是,我不太确定这段代码是线程安全的。这段代码是否可能容易出现竞争条件,取消处理程序与事件处理程序同时执行但在相应事件DispatchWorkItem被取消之前执行?

我意识到我可能可以添加锁,或者使用串行队列,我的问题是向熟悉libdispatch内部结构的人提问。

4

1 回答 1

1

您可能知道,这个libDispatch代码非常密集。虽然通过它可能会有所启发,但人们应该不愿意依赖实施细节,因为它们可能会在没有警告的情况下发生变化。一个人应该完全依赖文档中所述的正式保证。幸运的是,setCancelHandler 文档提供了这样的保证:

cancel()一旦系统释放了对源的底层句柄的所有引用并且源的事件处理程序块已经返回,取消处理程序(如果指定)被提交到源的目标队列以响应对该方法的调用。

因此,在回答您的事件/取消竞赛时,文档告诉我们,取消处理程序只会在“事件处理程序块返回”后被调用。</p>


这也可以通过经验验证,通过明智地插入sleep呼叫来显示潜在的竞争。考虑您的第二个示例的这种再现:

logger.log("starting timer")

let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timerQueue", attributes: .concurrent)
let timer = DispatchSource.makeTimerSource(queue: queue)

let cancelHandler = DispatchWorkItem {
    logger.log("cancel handler")
}

timer.setEventHandler {
    logger.log("event handler started")
    Thread.sleep(forTimeInterval: 2)           // manifest potential race
    cancelHandler.cancel()
    logger.log("event handler finished")
}

timer.setCancelHandler(handler: cancelHandler)
timer.schedule(wallDeadline: .now() + 1)
timer.activate()

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    logger.log("canceling timer")
    timer.cancel()
}

logger.log("done starting timer")

这会产生:

2021-10-05 11:29:45.198865-0700 MyApp[18873:4847424] [ViewController] starting timer
2021-10-05 11:29:45.199588-0700 MyApp[18873:4847424] [ViewController] done starting timer
2021-10-05 11:29:46.199725-0700 MyApp[18873:4847502] [ViewController] event handler started
2021-10-05 11:29:47.387352-0700 MyApp[18873:4847424] [ViewController] canceling timer
2021-10-05 11:29:48.204222-0700 MyApp[18873:4847502] [ViewController] event handler finished

请注意,没有“取消处理程序”消息。

因此,简而言之,我们可以看到 GCD 解决了事件和取消处理程序之间的这种潜在竞争,如文档中所述。

于 2021-10-05T18:44:49.330 回答