Swift:推迟异步`Task`的教程

1,048 阅读3分钟

大多数情况下,我们希望我们的各种异步任务在创建后尽快开始,但有时我们可能想给它们的执行添加一点延迟--也许是为了给另一个任务先完成的时间,或者添加某种形式的 "脱跳 "行为。

虽然没有直接的、内置的方法来运行具有一定延迟的SwiftTask ,但我们可以通过告诉任务在我们实际开始执行其操作之前睡眠一定数量的纳秒来实现这种行为。

Task {
    // Delay the task by 1 second:
    try await Task.sleep(nanoseconds: 1_000_000_000)
    
    // Perform our operation
    ...
}

调用Task.sleep 与使用sleep 系统函数等非常不同,因为Task 版本与其他代码相比是完全非阻塞的。

上述对Task.sleep 的调用被标记为try 关键字的原因是,如果任务在其睡眠时间内被取消,该调用将抛出一个错误。因此,举例来说,如果我们想让一个视图控制器只在一个异步操作超过150毫秒才完成时显示一个加载旋钮,那么我们可以实现这样的东西:

class VideoViewController: UIViewController {
    ...
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let loadingSpinnerTask = Task {
            try await Task.sleep(nanoseconds: 150_000_000)
            showLoadingSpinner()
        }

        Task {
            await prepareVideo()
            loadingSpinnerTask.cancel()
            hideLoadingSpinner()
        }
    }
    
    ...
}

请注意,以上并不意味着是一个完整的例子,如何使用Task 来加载视图控制器的内容。例如,我们可能想在开始一个新的加载任务之前检查现有的加载任务是否已经在进行中。要了解更多,请查看"任务在Swift的并发系统中扮演什么角色?"。

现在,如果我们要在给定的代码库中使用大量的延迟任务,那么可能值得定义一个简单的抽象,让我们更容易地创建这样的延迟任务--例如,让我们使用更标准的TimeInterval 值来定义基于秒的延迟,而不是必须使用纳秒:

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

我们必须明确地将我们的sleep 任务标记为Task<Never, Never> ,是因为该方法只在那个确切的Task 专用上可用,而在我们的扩展范围内,符号Task 是指我们的扩展正在使用的当前专用。

有了上述扩展,我们现在只要想创建一个延迟的任务,就可以简单地调用Task.delayed 。这种方法的唯一缺点是,我们现在必须在这些任务闭包中手动捕获self :

class VideoViewController: UIViewController {
    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let loadingSpinnerTask = Task.delayed(byTimeInterval: 0.15) {
            self.showLoadingSpinner()
        }

        Task {
            await prepareVideo()
            loadingSpinnerTask.cancel()
            hideLoadingSpinner()
        }
    }
    
    ...
}

不过有一个方法可以解决这个小问题--那就是使用 "半公开 "的_implicitSelfCapture 属性--Swift 标准库就是用它来让所有内置的Task 闭包自动捕获self 引用:

extension Task where Failure == Error {
    static func delayed(
        byTimeInterval delayInterval: TimeInterval,
        priority: TaskPriority? = nil,
        @_implicitSelfCapture operation: @escaping @Sendable () async throws -> Success
    ) -> Task {
        ...
    }
}

然而,由于上述属性还不是Swift公共API的正式组成部分(考虑到它的名字前面有一个下划线),我并不推荐在生产代码中使用它--除非你愿意接受任何使用它的代码可能在任何时候发生故障的风险。

我希望你喜欢这个快速浏览使用Swift新的内置Task API对延迟操作进行建模的几种不同方式。当然,我们也可以选择使用旧的工具来实现这种延迟--比如Grand Central Dispatch、计时器,甚至是Objective-C运行时(至少在某些情况下)。但是,当使用Swift的新并发系统编写异步代码时,能够直接使用Task 类型来实现这种延迟行为可能真的很方便。

谢谢你的阅读!