Swift之深入解析异步函数async/await的使用与运行机制
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之深入解析异步函数async/await的使用与运行机制相关的知识,希望对你有一定的参考价值。
一、async/await
- 在 WWDC21 中 Swift 盼来了 async/await,作为现代编程语言的标志之一,async/await 可以让我们像编写常规代码一样,轻松地编写异步代码,这样能更直观且更安全地表达我们的思路。
- async/await 是整个 Swift 结构化并发的基础,从这个 Session 开始,一起来探索 Swift 新的并发框架。
二、加载缩略图步骤
- 如下图所示, Session 通过加载缩略图片演示了 async/await 的使用:
- 加载缩略图片分为以下几个步骤:
-
- 从 URL 字符串创建一个 URLRequest 对象;
-
- URLSession 的 dataTask(with:completion:) 方法获取要请求图片数据;
-
- UIImage(data:) 从图片数据中创建一个图像;
-
- UIImage 的 prepareThumbnail 方法从原始图像中渲染一个缩略图。
- 这些操作的每一步都依赖于前一个步骤的结果,所以必须按顺序执行。在这四步操作中,第二步和第四步会比较费时,所以这两步操作一般通过异步调用来完成。
三、原始版本代码
- 原始的调用方式如下所示,定义一个 fetchThumbnail 函数,其中有一个 completion 参数,用于将输出返回给调用者:
- fetchThumbnail 函数体中规中举地按上文中的步骤来完成任务:
-
- 调用 thumbnailURLRequest,该操作是同步的;
-
- 调用 URLSession 的 dataTask(with:completion:),这是个异步操作,需要调用 resume() 方法启动异步工作;fetchThumbnail 返回执行其它任务;
-
- 在 dataTask(with:completion:) 的完成处理程序中,处理获取数据后的操作;
-
- 正常获取数据的情况下,通过 UIImage(data:) 来创建一个图像,这个操作是同步的;
-
- 调用 UIImage 对象的 prepareThumbnail 方法生成缩略图,该方法也是个异步方法。
- 这里有两个明显的问题:
-
- 必须时刻注意在哪里需要调用 completion(这里就有 5 处),如果忘记调用 completion 来通知调用者失败(Swift 不能保证强制执行 completion),则可能导致流程异常;
-
- 这里大约 20 行的代码,看似是按流程来走,但层层嵌套会让代码显示更加晦涩(所谓的回调地狱)。
- 现有的改进方案,效果都并不理想,如下:
-
- completion 的入参可以使用标准库的 Result,会更安全一点,但也只是一点点;
-
- 类 future 方法,但代码没有更简洁和安全。
四、async/await 版本代码
- async/await 版本代码如下所示:
- 函数签名发生了如下的明显变化:
-
- 入参不再的 completion 处理程序;
-
- 返回值是 UIImage 类型,表示函数返回的缩略图,同时通过关键字 throws 标识可以抛出一个异常;
-
- 在 throws 前面添加了关键字 async,表明这是一个异步函数(注意:如果没有 throws 关键字,则 async 直接放在 -> 前面)。
- fetchThumbnail 的实现更加简洁明了:
-
- 调用依然从 thumbnailURLRequest 开始,该操作是同步的,阻塞线程;
-
- 调用 URLSession 的 data(for:) 开始下载数据,这里有几个变化:
-
-
- 使用 await 标记方法调用,表明这是一个异步操作;如果一个表达式里面有多个异步函数调用,则只需要写一次 await;
-
-
-
- data(for: ) 方法是可等待的,调用后,会挂起自己,解除线程阻塞;
-
-
-
- 使用 try 是因为 fetchThumbnail 被标记为 throws,如果网络请求有异步,则直接抛出异常;
-
-
-
- data(for:) 完成后,恢复 fetchThumbnail,并将返回的数据及请求响应赋值给 data 和 response,就像普通的赋值操作一样。
-
-
- 判断响应是否有效,如果响应码不为 200,则抛出异步,这一步为同步操作;
-
- 正常获取数据后,通过 UIImage(data:) 来创建一个图像,这个操作是同步的;
-
- 调用 UIImage 对象的 thumbnail 属性生成缩略图,这是个可等待的属性(此处为非 SDK 内置的属性,而是自定义的属性),这里同样使用 await 来标识异步操作;如果 thumbnail 失败,则抛出一个异常。
- 我们可以看到,短短的 6 行代码就实现了上面大约 20 行代码的功能,优点显而易见:
-
- 更安全:整个过程能确保出错时抛出异常;
-
- 更简洁:避免的代码的层层嵌套;
-
- 更能体现意图:整个代码基本是和我们预定的流程保持了一致。
五、可等待属性
- 如下代码,是上文中使用的 thumbnail 属性的实现,它是 UIImage 的一个扩展属性:
- 异步属性必须是只读的,可写属性不能声明为异步属性;
- 异步属性需要有一个明确的 getter,async 关键字位于 get 后;
- 从 Swift 5.5 开始,getter 也可以抛出异常,如果同时是异步的,则 async 关键字位于 throws 前面;
- await 可用于属性 body 中的表达式,以表明操作的异步性。
六、调用流程对比
- 普通函数的调用流程如下图所示:
-
- 调用函数;
-
- 函数获取线程的控制权,并完全占用该线程;
-
- 函数执行完成返回或者抛出错误,将控制权交还调用方。
- 普通函数放弃线程控制权的唯一方式就是执行完成,而异步函数的调用流程则如下图所示:
- 异步函数的调用流程分析:
-
- 调用函数;
-
- 函数获得线程控制权;
-
- 函数运行后,挂起,同时放弃对线程的控制,并将控制权交给系统,系统可自由支配该线程;
-
- 系统确定何时恢复函数;
-
- 函数恢复后重新获得控制权,并继续工作;
-
- 函数执行完成或抛出异常后,返回调用方,将控制权交还给调用方。
- 需要注意的是:
-
- 一个异步函数挂起时,也会挂起它的调用者,所以调用者也必须是异步的;
-
- 异步函数可以多次挂起,就像上面的 fetchThumbnail 方法一样使用了两个 await 关键字;
-
- 异步函数挂起时,不会阻塞线程;
-
- 异步函数可能会在一个完全不同的线程上恢复;
-
- async 函数并不一定会挂起。
七、单元测试
- 以往,想要对网络请求任务做一些单元测试时,需要借助 XCTestExpectation 这个类。如下:
- 有了 async/await 后,异步函数的单元测试更加简单了:
八、异步任务
-上文中提到,异步函数的调用者也需要是异步函数。但有些情况下,确实需要在同步方法中调用异步函数,这时就需要用到异步任务功能。即如下代码,通过 async 闭包将任务打包,以执行异步操作:
九、代码迁移
- 对于老代码,更关注的是如何更好地使用上新特性。对于 async/await,Swift 团队也为我们做了很多准备:
- 提供了大量的异步 API,以替代采用完成处理程序的 API:
- 许多委托方法也有相应的异步替代方法。
十、Continuation 模式
- Swift 是如何与系统协作,完成异步代码的恢复呢?答案就是 Continuation 模式,方法的调用者等待函数调用的结果并提供一个闭包来指定下一步要做什么。当函数调用完成时,调用完成处理程序恢复调用者想要对结果执行的操作,这种协同执行正是 Swift 中异步函数的工作方式。
- 为此,Swift 提供了 withCheckedThrowingContinuation 函数:
Suspends the current task, then calls the given closure with a checked throwing continuation for the current task.
- 以及 CheckedContinuation 结构体:
A mechanism to interface between synchronous and asynchronous code, logging correctness violations.
- 通过这些结构体和函数,调用方可以访问可用于恢复挂起的异步函数的延续值。CheckedContinuation 结构体还提供了多个 resume 方法,用来回传结果值。
- Continuation 提供了一种强大的方式来手动控制异步函数的执行,不过有一点需要记住:resume 在每个代码分支上必须且只能调用一次。
- 如果某个分支没有调用 resume,异步调用将永远挂起;而如果某个分支调用了多次,则可能会破坏程序数据,这两种情况 Swift 都会给出警告或错误。
以上是关于Swift之深入解析异步函数async/await的使用与运行机制的主要内容,如果未能解决你的问题,请参考以下文章