3、Swift协程详解:调用协程
我们现在已经知道怎么定义异步函数了,也可以很轻松的转换将现有的异步回调 API 转成异步函数。那下一个问题就是,既然普通函数不能调用异步函数,那定义好的这些异步函数该从哪儿开始调用呢?
使用 Task
Task 的创建
其实从上一节我们分析如何将回调转成异步函数的时候就已经发现,异步函数的关键在于 Continuation。所以,只要调用异步函数的位置能让异步函数获取到 Continuation,那么调用异步函数的问题就解决了。Swift 标准库提供了 Task 类来提供这个能力。
我们给出 Task 的构造器的定义:
public init( priority: _Concurrency.TaskPriority? = nil, operation: @escaping @Sendable () async -> Success) public init( priority: _Concurrency.TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success)
它接收一个异步闭包作为参数,创建一个 Task 实例并运行这个异步闭包。而在这个闭包当中,我们就可以调用任意异步函数了:
Task { let result = await helloAsync() print(result) }
除了直接构造 Task 之外,还可以调用 Task 的 detach 函数来创建一个不一样的 Task:
Task.detached (operation: { await helloAsync() })
这个函数返回的也是一个 Task 实例,我们不妨看一下它的定义:
public static func detached( priority: _Concurrency.TaskPriority? = nil, operation: @escaping @Sendable () async -> Success ) -> _Concurrency.Task<Success, Failure> public static func detached( priority: _Concurrency.TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success ) -> _Concurrency.Task<Success, Failure>
注意到它其实是 Task 的静态函数,返回值正是 Task 类型。
两种 Task 的对比
那通过 detached 函数创建的 Task 和直接使用 Task 的构造器创建的 Task 实例有什么不同呢?我们先来看一下文档的说明:
detached 函数的部分注释
/// Runs the given nonthrowing operation asynchronously /// as part of a new top-level task.
Task 类的 init 的部分注释
/// Runs the given nonthrowing operation asynchronously /// as part of a new top-level task on behalf of the current actor.
可以看到这两段说明有一个共同点:通过二者创建的 Task 都是 top-level task。这是什么意思呢?这个其实是与在 TaskGroup 当中创建子任务是相对应的,前面介绍的这两种方式创建出来的任务都是顶级任务,没有父任务。TaskGroup 的内容我们下一篇文章再介绍。
接下来就是区别点了,即使用 Task 直接构造的任务实例会 on behalf of the current actor
。Actor 我们还没有介绍,不过我们姑且理解为任务启动时所在的运行环境。这里主要包括挂起的异步函数在恢复时如何调度,以及对于 TaskLocal 变量的感知上。这些内容我们后面会专门写文章介绍。
简单来说,通过 Task { ... }
创建的任务会对外界的状态有感知,而通过 Task.detached { ... }
创建的任务就完全是个孤儿了 —— 也正是因为这一点,官方文档里面也提醒我们一般情况下不要使用 detached 来创建任务。
以上创建 Task 的方式,也被称为非结构化并发。
这里并发的意思是,Task 都会把自己的代码块传给一个后台异步队列去执行。非结构化则与添加到 TaskGroup 当中的任务相对应,添加到 TaskGroup 当中的任务的形式被称为结构化并发,这些 Task 会随着整个 TaskGroup 的取消而取消,而相对应地,顶级任务的状态管理都只与自己有关,想要取消也必须调用 Task 的 cancel 显式地对任务进行取消。
现在你应该对 TaskGroup、Actor、TaskLocal 之类的概念也产生了兴趣,如果不能理解,也先不着急,我们等后面再慢慢展开介绍。
不管怎样,讲到这里,我们已经知道如何在程序当中使用异步函数了,下面我们给出一个完整的命令行程序:
func helloAsync() async -> Int { await withCheckedContinuation { continuation in DispatchQueue.global().async { continuation.resume(returning: Int(arc4random())) } } } Task.detached { print(await helloAsync()) } Task { print(await helloAsync()) } // 主线程等待 1s,防止程序提前退出导致异步任务没有执行 Thread.sleep(forTimeInterval: 1)
运行这个程序可以得到:
1804289383 846930886
嗯,这是两个随机数。在这个例子当中,我们既没有定义 Actor,也没有定义 TaskLocal,因此创建出来的两个 Task 其实是没有什么本质的区别的。
说明:Swift 的协程需要 macOS 12.0,iOS 15.0 及以上版本才可以运行,因此大家可以在 iOS 15.0 的设备或者模拟器上体验异步函数的调用。有趣的是,在 Windows 和 Linux 上安装 Swift 5.5 的编译器之后,上述程序是可以运行的。
Task 的结果
Task 的闭包有返回值作为它的结果返回。由于 Task 是异步执行的,它的结果自然也是异步的:
// Task public var value: Success { get async throws }
我们可以在其他异步函数当中使用 await 来获取它的结果:
let task = Task { await helloAsync() } print(try await task.value)
由于 Task 的闭包可以抛出异常,因此对于每一个 Task 来讲,异常也是结果的一种可能。如果我们只是任性地启动了一个 Task 而不去获取它的结果的话,Task 内部抛出的任何异常都与外部无关:
func errorThrown() async throws { throw "Runtime Error" } func taskWithError() async throws { let task = Task { try await errorThrown() } // 避免程序过早退出,等 1s await Task.sleep(1000_000_000) }
如果我们想要看看 Task 究竟抛出了什么异常,我们可以在读取它的 value 时对异常进行捕获:
func taskWithError() async throws { let task = Task { try await errorThrown() } do { try await task.value } catch { print(error) } }
我们前面定义的 Task 时传入的闭包会抛异常,这样一来 Task 的第二个泛型参数 Failure 就不可能是 Never。这种情况下获取 value 的操作需要使用 try 关键字。
异步 main 函数
通过创建 Task 的方式适用于所有在同步函数当中需要调用异步函数的情形。当然,对于命令行程序来讲,我们还可以直接把 main 函数定义为 async 函数:
App.swift
@main struct App { static func main() async throws { ... } }
首先我们定义一个结构体(或者类),将其标注为 @main;接着定义一个静态的 main 函数,这个函数可以是同步函数也可以是异步函数。
注意,通过这种方式,main.swift 文件要留空(或者直接删掉)。
这样我们就可以愉快地调用异步函数了:
import Foundation @main struct App { static func main() async throws { print(await helloAsync()) let detachedTask = Task.detached { () -> Int in print(await helloAsync()) return 1 } let task = Task { () -> Int in print(await helloAsync()) return 2 } print("detached task result: \(try await detachedTask.value)") print("task result: \(try await task.value)") } }
说明:异步 main 函数同样受到 macOS 运行时版本的限制,但在 Windows 和 Linux 上不受限制。
小结
本文我们主要介绍了如何创建调用异步函数的条件的问题,大家也可以自己体验一下 Swift 的协程了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)