在GCD中,我只需呼叫:

DispatchQueue.main.asyncAfter(deadline: .now() + someTimeInterval) { ... }

但我们开始迁移到 struct 化并发.

我试了following code:

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的情况下,结果时间间隔等于某个TimeInterval,但对于 struct 化并发,时间间隔要比指定的时间间隔大得多.如何解决此问题?

Minimal reproducible example

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
    }
}

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

推荐答案

在标题中,您问:

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,将其包装在withCheckedThrowingContinuationwithTaskCancellationHandler中.


正如你在上面看到的,回旋余地/容忍度问题与它在哪个演员身上的问题完全不同.

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并发性的目标之一是简化我们的逻辑,完全消除转义闭包.

在没有看到更多细节的情况下,我们无法建议如何最好地重构调用点,但这通常很容易.但这将是另一个问题.

Swift相关问答推荐

可编码:将字符串解码为自定义类型(ISO 8601日期,无时间组件)

在SWIFT中使用Objective-C struct 时出错(在作用域中找不到类型)

带DispatchSourceTimer的定时器

当将新值推送到 NavigationStack 时,SwiftUI 的 navigationDestination 已在堆栈的早期声明

在Swift中如何将NSUrl转换为本地路径URL

CardStack 和 Lottie

为什么 XUIView 中的 Binding 看不到更新后的值?

在 init(from decoder: Decoder) 方法中访问原始 JSON 数据

如何延迟 swift 属性 didSet 使其每秒只触发一次

在 SwiftUI 中将属性显式设置为其默认值

为什么 id 不能通过 struct 从 Objective-C 移植到 Swift?

`let case(value)` 和 `case(let value)` 之间的区别 (Swift)

来自数据的 SwiftUI 图像

不要从 NumberFormatter 获取货币符号

展开 List 中的 HStack 以端到端但不是从上到下,因此看起来项目之间有更宽的空间

单元测试和私有变量

Swift 3:日期与 NSDate?

使用 phimagemanager 将图像保存到自定义相册?

快速延迟加载属性

枚举大小写的原始值必须是文字