在标题中,您问:
SWIFT中的 struct 化并发性相当于DispatchQueue.main.asyncAfter
?
从SE-0316中的例子推断,字面上的类似功能就是:
Task { @MainActor in
try await Task.sleep(for: .seconds(5))
foo()
}
或者,如果已经从异步上下文调用它,如果您正在调用的 routine 已经隔离到主要参与者,则不需要使用Task {…}
引入unstructured concurrency:
try await Task.sleep(for: .seconds(5))
await foo()
与传统的sleep
API不同,Task.sleep
不会阻塞调用者,因此通常不需要将其包装在非 struct 化任务Task {…}
中(并且我们应该避免不必要地引入非 struct 化并发).这取决于你所说的文本.参见WWDC 2021视频Swift concurrency: Update a sample app,其中展示了人们如何使用MainActor.run {…}
,以及对主要角色的隔离功能如何经常使这一点变得不必要.
你说过:
当我比较输出中的两个日期时,它们的差异远远超过5秒.
我想这取决于你所说的"更多"是什么意思.例如,当我睡5秒钟时,我通常会看到它需要5.2秒:
let start = ContinuousClock.now
try await Task.sleep(for: .seconds(5))
print(start.duration(to: .now)) // 5.155735542 seconds
因此,如果您看到它花费的时间超过much,那么这只是表明您有其他东西阻止了该参与者,这个问题与手边的代码无关.
然而,如果你只是想知道它怎么会慢到几分之一秒,这似乎是默认的容忍策略.正如并发头所说的:
这一容忍预计将成为绕过
截止日期.时钟可以在容差内重新调度任务以确保
通过减少潜在的操作系统来高效执行恢复
起床了.
如果您需要较小的容忍度,请考虑使用新的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视频Meet Swift Async 算法rithms.如果您试图向后支持较早的操作系统版本,并且确实需要更少的回旋余地,那么您可能不得不退回到传统的API,将其包装在withCheckedThrowingContinuation
和withTaskCancellationHandler
中.
正如你在上面看到的,回旋余地/容忍度问题与它在哪个演员身上的问题完全不同.
But let us turn to your global
queue question. 你说过:
但这似乎等同于:
DispatchQueue.global().asyncAfter(deadline: .now() + someTimeInterval) {
DispatchQueue.main.async { ... }
}
通常,当您在参与者隔离的上下文中运行Task {…}
时,这是一个代表当前参与者运行的新的顶级非 struct 化任务.但delayed
并不是演员孤立的.而且,从SWIFT 5.7开始,SE-0338已经正式制定了非参与者隔离方法的规则:
async
个不是参与者隔离的函数应该在没有参与者关联的通用执行器上正式运行.
鉴于此,将其比作global
个调度队列是公平的.但为了保护作者,他的帖子被标记为SWIFT 5.5,而SE-0338是在SWIFT 5.7中引入的.
我可能倾向于将这种独立的行为明确地表达出来,并达到detached
个任务("不属于当前参与者的unstructured个任务"):
extension Task where Failure == Error {
/// Launch detached task after delay
///
/// - Note: Don’t use a detached task if it’s possible to model the
/// operation using structured concurrency features like child tasks.
/// Child tasks inherit the parent task’s priority and task-local storage,
/// and canceling a parent task automatically cancels all of its child
/// tasks. You need to handle these considerations manually with
/// a detached task.
///
/// You need to keep a reference to the detached task if you want
/// to cancel it by calling the Task.cancel() method. Discarding your
/// reference to a detached task doesn’t implicitly cancel that task,
/// it only makes it impossible for you to explicitly cancel the task.
@discardableResult
static func delayed(
byTimeInterval delayInterval: TimeInterval,
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success
) -> Task {
Task.detached(priority: priority) { // detached
let delay = UInt64(delayInterval * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: delay)
return try await operation()
}
}
}
IMHO,使用分离的任务使行为显式且毫不含糊.我建议内联文档传达与detached
documentation完全相同的警告/注意事项.当引入分离任务时,应用程序开发人员应该知道他们要注册的是什么.
你说过:
在GCD中,我只需呼叫:
DispatchQueue.main.asyncAfter(deadline: .now() + someTimeInterval) { ... }
但我们开始迁移到 struct 化并发.
如果你真的想要能做到这一点的东西,你可以这样做:
extension Task where Failure == Error {
@discardableResult
@MainActor
static func delayedOnMain(
byTimeInterval delayInterval: TimeInterval,
priority: TaskPriority? = nil,
operation: @escaping @MainActor () async throws -> Success
) -> Task {
Task(priority: priority) { [operation] in
let delay = UInt64(delayInterval * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: delay)
return try await operation()
}
}
}
这就把delayedOnMain
分成了男主角,operation
分成了operation
分.然后,您可以执行以下操作:
@MainActor
class Foo {
var count = 0
func bar() async throws {
Task.delayedOnMain(byTimeInterval: 5) {
self.count += 1
}
}
}
这样,在呼叫点不需要MainActor.run {…}
.
话虽如此,与其像上面那样提出DispatchQueue.main.asyncAfter
的直接模拟,你可能会看看是否可以完全重构出来.SWIFT并发性的目标之一是简化我们的逻辑,完全消除转义闭包.
在没有看到更多细节的情况下,我们无法建议如何最好地重构调用点,但这通常很容易.但这将是另一个问题.