在 GCD 中我只是调用:
DispatchQueue.main.asyncAfter(deadline: .now() + someTimeInterval) { ... }
但是我们开始迁移到结构化并发。
我尝试了以下代码:
extension Task where Failure == Error {
static func delayed(
byTimeInterval delayInterval: TimeInterval,
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success
) -> Task {
Task(priority: priority) {
let delay = UInt64(delayInterval * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: delay)
return try await operation()
}
}
}
用途:
Task.delayed(byTimeInterval: someTimeInterval) {
await MainActor.run { ... }
}
但它似乎相当于:
DispatchQueue.global().asyncAfter(deadline: .now() + someTimeInterval) {
DispatchQueue.main.async { ... }
}
因此,在使用 GCD 的情况下,结果时间间隔等于 someTimeInterval,但使用结构化并发时间间隔则远大于指定的时间间隔。如何解决这个问题?
最小可重现示例
extension Task where Failure == Error {
static func delayed(
byTimeInterval delayInterval: TimeInterval,
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success
) -> Task {
Task(priority: priority) {
let delay = UInt64(delayInterval * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: delay)
return try await operation()
}
}
}
print(Date())
Task.delayed(byTimeInterval: 5) {
await MainActor.run {
print(Date())
... //some
}
}
当我比较输出中的 2 个日期时,它们的差异远超过 5 秒。
在标题中,您问:
相当于 Swift 中的结构化并发?DispatchQueue.main.asyncAfter
从 SE-0316 中的示例推断,字面等效值就是:
Task { @MainActor in
try await Task.sleep(for: .seconds(5))
foo()
}
或者,如果已经从异步上下文调用此函数,并且您正在调用的例程已经与主要参与者隔离,则不需要使用 Task {…}
引入
非结构化并发:
try await Task.sleep(for: .seconds(5))
await foo()
与传统的
sleep
API 不同,Task.sleep
不会阻塞调用者,因此不需要经常将其包装在非结构化任务中,Task {…}
(我们应该避免引入不必要的非结构化并发)。这取决于您调用它的文本。请参阅 WWDC 2021 视频 Swift 并发:更新示例应用程序,其中展示了如何使用 MainActor.run {…}
,以及如何将功能隔离到主要参与者,甚至变得不必要。
你说:
当我比较输出中的 2 个日期时,它们的差异远超过 5 秒。
我想这取决于你所说的“更多”是什么意思。例如,当睡 5 秒时,我经常会看到它需要 ~5.2 秒:
let start = ContinuousClock.now
try await Task.sleep(for: .seconds(5))
print(start.duration(to: .now)) // 5.155735542 seconds
因此,如果您发现它花费的时间远长于这个时间,那么这只是表明您有其他东西阻碍了该参与者,这是一个与手头的代码无关的问题。
但是,如果您只是想知道它怎么可能会超过零点几秒,那么这似乎是默认的容忍策略。正如并发标题所说:
公差预计为周围的余地 最后期限。时钟可以在容差范围内重新安排任务,以确保 通过减少潜在的操作系统来有效地执行恢复 醒来。
如果您需要较小的容差,请像这样指定:
let start = ContinuousClock.now
try await Task.sleep(for: .seconds(5), tolerance: .zero)
print(start.duration(to: .now)) // 5.001445416 seconds
Clock
API:
let clock = ContinuousClock()
let start = ContinuousClock.now
try await clock.sleep(until: .now + .seconds(5), tolerance: .zero)
print(start.duration(to: .now)) // 5.001761375 seconds
不用说,操作系统在计时器中具有容差/回旋余地的全部原因是为了电源效率,因此只有在绝对必要的情况下才应该限制容差。在可能的情况下,我们希望尊重客户设备的功耗。
此 API 已在 iOS 16、macOS 13 中引入。有关更多信息,请参阅 WWDC 2022 视频认识 Swift 异步算法。如果您试图为早期操作系统版本提供向后支持并且确实需要更少的余地,您可能必须回退到旧版 API,将其包装在
withCheckedThrowingContinuation
和 withTaskCancellationHandler
中。
正如您在上面所看到的,余地/容忍度问题与它所在的演员的问题完全分开。
但是让我们转向您的
global
队列问题。你说:
但它似乎相当于:
DispatchQueue.global().asyncAfter(deadline: .now() + someTimeInterval) { DispatchQueue.main.async { ... } }
通常,当您从参与者隔离的上下文中运行
Task {…}
时,这是代表当前参与者运行的新的顶级非结构化任务。但 delayed
并不是演员孤立的。并且,从 Swift 5.7 开始,SE-0338 已经正式化了非参与者隔离的方法的规则:
未与参与者隔离的函数应在与参与者无关的通用执行器上正式运行。async
鉴于此,可以将其类比为
global
调度队列。但作者辩称,他的帖子被标记为 Swift 5.5,SE-0338 是在 Swift 5.7 中引入的。