本文基于 Session 10132[1] / 10134[2] / 10133[3] 梳理,使用「获取缩略图」的例子对 Swift 5.5 新增的异步与并发编程进行讲解,如需了解更多,欢迎查看其它 Session。
异步编程在 iOS 开发里是一个常见的操作,例如我们经常需要在网络请求回来之后更新数据模型和视图。但是当异步操作嵌套时,不仅容易出现逻辑错误,还可能会陷入回调地狱。
completion
回调我们用「获取缩略图」来举例,先看下下面这段代码,找下有哪些逻辑问题:
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
是的,少部分人可以发现,以上代码有两处错误,都是在 guard
语句之后直接退出,没有调用 completion
,这样外部调用就无法处理所有可能的情况。而且不调用 completion
是一个合法(但不符合预期)的行为,编译器不会产生错误。
把遗留的两个 completion
补全后,代码如下:
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
completion(nil, FetchError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage)
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
为了处理这些异步函数的结果,需要使用五个 completion
来通知调用方,并且调用方需要判断 UIImage?
和 Error?
的 4 种组合结果,但实际上有效的只有 2 种。
我们先用 Swift 标准库里面的 Result
来解决这个问题,代码如下:
func fetchThumbnail(for id: String, completion: @escaping (Result<UIImage, Error>) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(.failure(FetchError.badID))
} else {
guard let image = UIImage(data: data!) else {
completion(.failure(FetchError.badImage))
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(.failure(FetchError.badImage))
return
}
completion(.success(thumbnail))
}
}
}
task.resume()
}
只要调用方可以知道结果,那么结果一定只有 2 种,要么成功,要么失败,不需要判断其他情况,更加符合使用习惯,但是这样仍然无法避免 completion
被忽略调用的情况。
async
和 await
重构函数为了解决 completion
被忽略调用的情况,我们可以尝试使用 Swift 5.5 新增的 async
和 await
来解决这个问题,代码如下:
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage }
return thumbnail
}
步骤非常简单:
request
请求,这是一个同步操作;try
标记可能抛出错误,await
标记潜在暂停点,这是一个异步操作;await
标记潜在暂停点,无法生成则抛出错误,这是一个异步操作;相较于前面 completion
版本的 23 行,这里 async
版本仅需要 8 行,没有函数的层层嵌套,都是一步接一步线性的,易读性大大提升,同时编译器也可以检查错误。
a normal function call
上图是一个普通函数调用,当 thumbnailURLRequest
完成之后,返回 fetchThumbnail
继续执行后续代码,如果这个函数是一个耗时任务,那么当前线程就会持续等待,直到完成。
an asynchronous function call
上图是一个异步函数调用,当 data(for:)
调用后,函数被挂起进行等待,当任务完成后,系统恢复 data(for:)
的调用,返回 fetchThumbnail
继续执行后续代码。
async / await facts
async
/ await
实际上的意思:
async
允许一个函数被挂起;await
标记一个异步函数的潜在暂停点;await
之后恢复执行。也就是说,我们用 async
标记一个函数是异步函数,这仅仅是一个标记,并不意味着函数一定是异步操作,函数内可以是一个简单的加法(例如 1 + 1
),也可以是一个异步的网络请求。当我们调用 async
函数时,需要使用 await
关键字进行调用,await
标记一个潜在暂停点。异步函数在执行同步代码时,不能放弃自己的线程,当执行到潜在暂停点时,会放弃自己的线程,挂起并等待被调用的异步函数的结果。当被调用的异步函数完成时,控制返回原来的异步函数的潜在暂停点(需要注意这时的线程不一定和之前的相同),继续执行之后的代码。这些线程切换的工作不需要我们手动处理,由 Swift 自动管理,极大地提高了编程效率。
async
/ await
的加入让我们得以使用与同步编程类似的控制流来进行异步编程,不仅可以解决 completion
容易被忽略的问题,还可以更好地进行错误处理。与此同时,线性的控制流也避免了回调地狱的问题,大大提高代码的可读性。
上个章节我们介绍了如何获取单张图片的缩略图,但在我们日常开发中更多的是展示列表,这就需要同时处理多张缩略图。
completion
回调处理多个缩略图如果我们一个接着一个地处理多个缩略图,可能会写出以下代码:
func fetchThumbnails(
for ids: [String],
completion handler: @escaping ([String: UIImage]?, Error?) -> Void
) {
guard let id = ids.first else { return handler([:], nil) }
let request = thumbnailURLRequest(for: id)
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response,
let data = data
else {
return handler(nil, error)
}
UIImage(data: data)?.prepareThumbnail(of: thumbSize) { image in
guard let image = image else {
return handler(nil, ThumbnailFailedError())
}
fetchThumbnails(for: Array(ids.dropFirst())) { thumbnails, error in
// 添加图片到 thumbnails
}
}
}
dataTask.resume()
}
代码非常糟糕,递归调用依次获取缩略图,并且难以整合这些缩略图。除此之外,代码可读性也很低。
async
和 await
处理多个缩略图我们参考上个章节对单张缩略图的处理,试着使用 async
和 await
进行重构,代码如下:
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
for id in ids {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
try validateResponse(response)
guard let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: thumbSize) else {
throw ThumbnailFailedError()
}
thumbnails[id] = image
}
return thumbnails
}
步骤非常简单:
thumbnails
保存返回值;for-in
循环创建任务;a . 创建请求;
b . 下载图片;
c . 检查资源合法性;
d . 生成缩略图;
e . 保存缩略图。
3 . 返回 thumbnails
。
可以发现,仅仅是增加了 for-in
循环,就可以非常方便地顺序处理多张缩略图。
async-let
处理结构化并发上面例子的 thumbSize
是本地提供的,处理起来比较简单。假设 thumbSize
也要从网络接口获取,那么在生成缩略图的过程中会有两个网络请求,代码如下:
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
let (data, _) = try await URLSession.shared.data(for: imageReq)
let (metadata, _) = try await URLSession.shared.data(for: metadataReq)
guard let size = parseSize(from: metadata),
let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
else {
throw ThumbnailFailedError()
}
return image
}
可以看到,在 imageReq
请求完成之后,才发起 metadataReq
请求,只有两个请求都完成之后,才能执行之后的代码。我们能不能让这两个请求同时进行呢?另外如果在两个请求和使用他们的返回结果之间有其他的无关任务,无关任务的执行必须等到两个请求完成之后,这无疑是一种浪费。
我们可以用 async-let
来解决这个问题,代码如下:
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
async let (data, _) = URLSession.shared.data(for: imageReq)
async let (metadata, _) = URLSession.shared.data(for: metadataReq)
guard let size = parseSize(from: try await metadata),
let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
else {
throw ThumbnailFailedError()
}
return image
}
使用 async let
标记 data
和 metadata
,使用 await
进行访问,如果函数可以抛出错误,那么还需要使用 try
关键字。两个网络请求会同时进行,并且执行后续代码,即上文提到的无关任务。当需要访问结果的时候,系统会进行等待直到完成或者抛出错误。这样便能更高效地完成整个任务。
需要注意的是,这两个网络请求是并发的,假如第一个获取图片的任务失败了抛出错误需要退出函数,Swift 会自动将未等待的任务标记取消,然后等待它完成再退出函数。任务被标记取消并不意味着停止任务,只是通知不需要该任务的返回值。
既然 for-in
内部的逻辑就是处理单个缩略图的逻辑,自然而然我们就会想到直接调用封装好的异步函数,代码如下:
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
for id in ids {
try Task.checkCancellation()
thumbnails[id] = try await fetchOneThumbnail(withID: id)
}
return thumbnails
}
你应该发现了,在获取缩略图之前,调用了 checkCancellation
来进行任务检查。如上文所述,子任务被标记取消并不会停止任务,我们可以尽可能地检查任务是否被取消,提前退出,不再请求后续的缩略图。
如果希望任务取消后返回之前已经处理的缩略图,可以使用 isCancelled
进行判断,代码如下:
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
for id in ids {
if Task.isCancelled { break }
thumbnails[id] = try await fetchOneThumbnail(withID: id)
}
return thumbnails
}
即使图片数据和元数据已经可以同时请求了,但是对于整个获取缩略图这个任务而言,仍然是一个接一个进行的,如果我们想要同时进行多个任务,就需要引入任务组进行并发编程,代码如下:
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
for id in ids {
group.async {
return (id, try await fetchOneThumbnail(withID: id))
}
}
for try await (id, thumbnail) in group {
thumbnails[id] = thumbnail
}
}
return thumbnails
}
步骤如下:
thumbnails
保存返回值;withThrowingTaskGroup
创建任务组;a . 使用 for-in
循环依次把任务放到任务组,任务被添加时会立刻执行,到达最大并发数量后进行等待;
b . 使用 for-await-in
循环依次取回已处理的缩略图,如果任务还没完成,等待直到完成,或者抛出错误。
3 . 返回 thumbnails
。
使用任务组可以非常方便地执行并发任务,线程切换和派发任务都由 Swift 进行管理,不需要我们编写复杂的控制逻辑。
async-let
与任务组的关系事实上,async-let
和任务组都有任务依赖树的概念,即一个父任务有一个或多个子任务,而这个父任务又是它的父任务的子任务。当一个子任务因为错误而导致父任务需要抛出错误退出时,这个子任务的兄弟任务会被标记取消,父任务需要等待这些任务完成或者抛出错误(但会忽略这些任务的返回值或者错误),才能真正退出。
async-let
可以看做简化版的任务组(实际上并不是任务组的语法糖),非常适合处理不同返回类型的异步任务,等待这些异步任务的返回结果,然后组合它们再继续后面的任务。
而任务组更适合处理数量不定的相同返回类型的异步任务,在遍历任务组的返回结果时可以使用一个或者多个返回结果。例如两个相同返回类型的任务,只需要处理第一个返回的结果,任务组非常方便,但 async-let
难以实现。
只有 async
/ await
的代码是不具备并发能力的,结构化并发的加入让我们得以用少量的代码在 async
/ await
的基础上实现并发编程,同时能够继续使用错误处理和线性控制流。
上两个章节我们介绍了异步与并发,语法简洁的同时功能强大。并发虽然高效,但是如果没有采取手段加以控制,很容易产生数据竞争的问题。
想象一下,下面代码的输出结果是什么:
class Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
let queue1 = DispatchQueue(label: "queue_1")
let queue2 = DispatchQueue(label: "queue_2")
queue1.async {
print(counter.increment())
}
queue2.async {
print(counter.increment())
}
1,1
、1,2
、2,1
、2,2
,都有可能,取决于 value
的写入和读取时机。
共享的可变状态会引发数据竞争,通常我们使用锁来解决数据竞争,但是锁的粒度不好控制,处理得不好还会引发死锁。
锁的粒度不好控制,也容易造成死锁,我们可以使用 Swift 5.5 新增的 actor
来解决数据竞争的问题。
那什么是 Actor
呢?(摘自 这里[4])
Actor
是一种并发模型,由状态(State)、行为(Behavior)、邮箱(Mailbox)三者组成。
actor
持有的变量,由自身管理,避免并发环境下的锁问题;actor
中的计算逻辑,通过 actor
接收到的消息来改变自身的状态;actor
之间通讯的桥梁,内部使用 FIFO
队列来存储和处理消息,接收方从邮箱中获取消息。Actor
模型描述了一组为避免并发编程问题的公理。
actor
状态都是本地的,外部无法访问;actor
之间必须通过消息传递进行通讯;actor
可以响应消息、退出新的 actor
、改变内部状态、把消息发送给一个或多个 actor
;actor
可能会阻塞自己但是不应该阻塞运行的线程。在 Swift 中,actor
是一种引用类型,并且不可以被继承。actor
内定义的变量和方法都是隔离的,在内部使用可以直接访问,而在外部需要使用 await
进行异步访问。对 actor
的变量访问和方法调用都是消息,在同一时刻只有一条消息可以被处理,多条消息按照顺序依次被处理。当某一条消息需要 await
等待其他异步任务时,消息会被挂起进行等待,actor
得以处理邮箱中的后续消息,表现为 actor
是可重入的。这就需要我们使用额外的机制来处理重复的请求,避免资源的浪费。
我们使用 actor
来解决 Counter
的数据竞争问题,代码如下:
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(await counter.increment())
}
asyncDetached {
print(await counter.increment())
}
结果要么是 1,2
,要么是 2,1
,不会存在 1,1
和 2,2
的情况。
我们试着使用 actor
实现一个图片下载器,代码如下:
actor ImageDownloader {
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from: url)
cache[url] = image
return image
}
}
当我们同时获取两种相同的图片时,图片下载器会先发起一个异步任务,当进行到 await
等待下载结果时,函数被挂起,从而发起第二个异步任务。假如先取回的图片是正确的,后取回的却是错误的,那么最终缓存下来的会是坏的。我们需要解决这类问题,一种解决方案是把先取回的当做正确的,后取回的忽略,代码如下:
actor ImageDownloader {
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from: url)
cache[url] = cache[url, default: image]
return cache[url]
}
}
更好的解决方案是把下载任务保存起来,先发起第一个异步任务并保存,如果在返回结果之前触发了第二次请求,那么就把上面保存的任务取出,并等待它完成。由于任务是同一个,所以只会发出一个网络请求,代码如下:
actor ImageDownloader {
private enum CacheEntry {
case inProgress(Task.Handle<Image, Error>)
case ready(Image)
}
private var cache: [URL: CacheEntry] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
switch cached {
case .ready(let image):
return image
case .inProgress(let handle):
return try await handle.get()
}
}
let handle = async {
try await downloadImage(from: url)
}
cache[url] = .inProgress(handle)
do {
let image = try await handle.get()
cache[url] = .ready(image)
return image
} catch {
cache[url] = nil
throw error
}
}
}
actor
的加入,让我们得以用一种新的方式来进行异步与并发编程,同时不需要处理数据竞争的问题。在 actor
内访问自有属性和方法都是同步逻辑,对外则表现为异步逻辑,同一时刻只能处理一条消息,多条消息顺序处理,这些规则为 actor
的高并发性提供了保证。
async
/ await
解决异步编程的问题;async
/ await
编写的代码不具有并发性,Swift 引入结构化并发让 async
/ await
编写的代码执行并发任务,同时解决一些控制流上的问题;actor
来解决共享可变状态的问题,状态被隔离在 actor
内部,修改只能通过向 actor
发送信息并等待结果,消息被 actor
以同步的方式进行处理,避免同一时间对数据进行修改。本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/45J58DWWGBCbGnPycVFp_Q
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。