DispatchQueue.main.asyncAfter 相当于 Swift 中的结构化并发?

Gar*_*rgo 11 multithreading grand-central-dispatch swift structured-concurrency

在 GCD 中我只是调用:

DispatchQueue.main.asyncAfter(deadline: .now() + someTimeInterval) { ... }
Run Code Online (Sandbox Code Playgroud)

但我们开始迁移到结构化并发。

我尝试了以下代码

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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

Task.delayed(byTimeInterval: someTimeInterval) {
    await MainActor.run { ... }
}
Run Code Online (Sandbox Code Playgroud)

但它似乎相当于:

DispatchQueue.global().asyncAfter(deadline: .now() + someTimeInterval) {
    DispatchQueue.main.async { ... }
}
Run Code Online (Sandbox Code Playgroud)

因此,在使用 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
    }
}
Run Code Online (Sandbox Code Playgroud)

当我比较输出中的 2 个日期时,它们的差异超过 5 秒。

Rob*_*Rob 17

在标题中,您问:

\n
\n

DispatchQueue.main.asyncAfterSwift 中的结构化并发等效吗?

\n
\n

从SE-0316中的示例推断,字面量相当于:

\n
Task { @MainActor in\n    try await Task.sleep(for: .seconds(5))\n    foo()\n}\n
Run Code Online (Sandbox Code Playgroud)\n

或者,如果已经从异步上下文调用此函数,并且您正在调用的例程已经与主要参与者隔离,则不需要引入非结构化并发Task {\xe2\x80\xa6}

\n
try await Task.sleep(for: .seconds(5))\nawait foo()\n
Run Code Online (Sandbox Code Playgroud)\n

与传统sleepAPI 不同,Task.sleep它不会阻塞调用者,因此通常Task {\xe2\x80\xa6}不需要将其包装在非结构化任务中(并且我们应该避免引入不必要的非结构化并发)。这取决于您调用它的文本。请参阅 WWDC 2021 视频Swift 并发:更新示例应用程序,该应用程序展示了如何使用MainActor.run {\xe2\x80\xa6},以及如何将函数隔离到主要参与者,甚至变得不必要。

\n
\n

你说:

\n
\n

当我比较输出中的 2 个日期时,它们的差异超过 5 秒。

\n
\n

我想这取决于你所说的 \xe2\x80\x9cmuch more\xe2\x80\x9d 的意思。例如,当睡眠 5 秒时,我经常会看到它需要大约 5.2 秒:

\n
let start = ContinuousClock.now\ntry await Task.sleep(for: .seconds(5))\nprint(start.duration(to: .now))                           // 5.155735542 seconds\n
Run Code Online (Sandbox Code Playgroud)\n

因此,如果您发现它花费的时间比这个长得,那么这只是表明您有其他东西阻碍了该参与者,这是一个与手头的代码无关的问题。

\n

但是,如果您只是想知道它如何会超过零点几秒,那么这似乎是默认的容忍策略。正如并发标题所说:

\n
\n

预计该容忍度是在截止日期之前的余地。时钟可以在容差范围内重新安排任务,以通过减少潜在的操作系统唤醒来确保恢复的有效执行。

\n
\n

如果您需要更少的容忍度,请考虑使用新的ClockAPI:

\n
let clock = ContinuousClock()\nlet start = ContinuousClock.now\ntry await clock.sleep(until: .now + .seconds(5), tolerance: .zero)\nprint(start.duration(to: .now))                           // 5.001761375 seconds\n
Run Code Online (Sandbox Code Playgroud)\n

不用说,操作系统在计时器中具有容差/回旋余地的全部原因是为了提高电源效率,因此只有在绝对必要的情况下才应该限制容差。在可能的情况下,我们希望尊重客户\xe2\x80\x99s 设备上的功耗。

\n

此 API 已在 iOS 16、macOS 13 中引入。有关更多信息,请参阅 WWDC 2022 视频认识 Swift 异步算法。如果您试图为早期操作系统版本提供向后支持并且确实需要更少的余地,那么您可能必须退回到旧版 API,将其包装在 awithCheckedThrowingContinuation和 a中withTaskCancellationHandler

\n
\n

正如您在上面所看到的,回旋余地/容忍度问题与它所在的演员的问题完全分开。

\n

但让我们转向您的global队列问题。你说:

\n
\n

但它似乎相当于:

\n
DispatchQueue.global().asyncAfter(deadline: .now() + someTimeInterval) {\n   DispatchQueue.main.async { ... }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

通常,当您Task {\xe2\x80\xa6}从参与者隔离的上下文中运行时,这是代表当前参与者运行的新的顶级非结构化任务。但delayed并不是演员孤立的。并且,从 Swift 5.7 开始,SE-0338正式规定了非参与者隔离的方法的规则:

\n
\n

async非参与者隔离的函数应在与参与者无关的通用执行器上正式运行。

\n
\n

鉴于此,可以将其类比为global调度队列。但在作者\xe2\x80\x99的辩护中,他的帖子被标记为Swift 5.5,SE-0338是在Swift 5.7中引入的。

\n

我可能倾向于使这种分离的行为明确并达到一个detached任务(\xe2\x80\x9can是非结构化任务,\xe2\x80\x99s 不是当前 actor\xe2\x80\x9d 的一部分):

\n
extension Task where Failure == Error {\n    /// Launch detached task after delay\n    ///\n    /// - Note: Don\xe2\x80\x99t use a detached task if it\xe2\x80\x99s possible to model the\n    /// operation using structured concurrency features like child tasks.\n    /// Child tasks inherit the parent task\xe2\x80\x99s priority and task-local storage,\n    /// and canceling a parent task automatically cancels all of its child\n    /// tasks. You need to handle these considerations manually with\n    /// a detached task.\n    ///\n    /// You need to keep a reference to the detached task if you want\n    /// to cancel it by calling the Task.cancel() method. Discarding your\n    /// reference to a detached task doesn\xe2\x80\x99t implicitly cancel that task,\n    /// it only makes it impossible for you to explicitly cancel the task.\n\n    @discardableResult\n    static func delayed(\n        byTimeInterval delayInterval: TimeInterval,\n        priority: TaskPriority? = nil,\n        operation: @escaping @Sendable () async throws -> Success\n    ) -> Task {\n        Task.detached(priority: priority) {                        // detached\n            let delay = UInt64(delayInterval * 1_000_000_000)\n            try await Task<Never, Never>.sleep(nanoseconds: delay)\n            return try await operation()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

恕我直言,使用分离任务使行为明确且明确。我建议使用内联文档来传达与detached 文档完全相同的警告/警告。应用程序开发人员在引入分离任务时应该知道他们正在注册什么。

\n
\n

你说:

\n
\n

在 GCD 中我只是调用:

\n
DispatchQueue.main.asyncAfter(deadline: .now() + someTimeInterval) { ... }\n
Run Code Online (Sandbox Code Playgroud)\n

但我们开始迁移到结构化并发。

\n
\n

如果你真的想要一些能做到这一点的东西,你可以这样做:

\n
extension Task where Failure == Error {\n    @discardableResult\n    @MainActor\n    static func delayedOnMain(\n        byTimeInterval delayInterval: TimeInterval,\n        priority: TaskPriority? = nil,\n        operation: @escaping @MainActor () async throws -> Success\n    ) -> Task {\n        Task(priority: priority) { [operation] in\n            let delay = UInt64(delayInterval * 1_000_000_000)\n            try await Task<Never, Never>.sleep(nanoseconds: delay)\n            return try await operation()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

delayedOnMain与主要演员以及operation. 然后你可以做这样的事情:

\n
@MainActor\nclass Foo {\n    var count = 0\n\n    func bar() async throws {\n        Task.delayedOnMain(byTimeInterval: 5) {\n            self.count += 1\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这样,MainActor.run {\xe2\x80\xa6}呼叫点就不需要任何操作。

\n

话虽如此,DispatchQueue.main.asyncAfter您可能会看到是否可以完全重构它,而不是像上面那样提出直接的模拟。Swift 并发的目标之一是简化我们的逻辑并完全消除逃逸闭包。

\n

如果没有看到更多细节,我们无法就如何最好地重构调用点提供建议,但这通常非常简单。但这是一个单独的问题。

\n