Support Statistics
¥.00 ·
0times
Text Preview (First 20 pages)
Registered users can read the full content for free
Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.
Page
1
(This page has no text content)
Page
2
Swift 异步与并发编程 王 巍 ObjC 中国 © 2021~ ObjC 中国
Page
3
Swift 异步与并发编程 1. 简介 1. ⽬标读者 2. 章节结构 3. 准备⼯作 2. Swift 并发初步 1. ⼀些基本概念 2. 异步函数 3. 结构化并发 4. actor 模型和数据隔离 5. ⼩结 3. 创建异步函数 1. 异步函数的动机 2. 转换函数签名 3. 使⽤续体改写函数 4. Objective-C ⾃动转换 5. Async getter 6. ⼩结 4. 异步序列 1. 同步序列和异步序列 2. 异步迭代器 3. 操作异步序列 4. AsyncStream 5. 异步序列和响应式编程 6. ⼩结
Page
4
5. 使⽤异步函数 1. ⽹络请求中的异步函数 2. Notification 3. 异步函数的运⾏环境 4. ⼩结 6. 结构化并发 1. 什么是结构化 2. 基于 Task 的结构化并发模型 3. ⾮结构化任务 4. ⼩结 7. 协作式任务取消 1. 任务取消到底做了什么 2. 处理任务取消 3. 取消的清理⼯作 4. 隐式等待和任务暂停 5. ⼩结 8. actor 模型和数据隔离 1. 共享内存模型的困境 2. Actor 隔离 3. Actor 协议 4. ⼩结 9. 全局 actor,可重⼊和 Sendable 1. 全局 actor 2. 可重⼊ 3. Sendable 4. ⼩结 10. 并发线程模型 1. 协同式线程池 2. 执⾏器
Page
5
3. 任务本地值和任务追踪 4. ⼩结 11. 总结和展望 1. 总结 2. 更新履历
Page
6
简介 在 Swift “七年之痒” 的 2021 年,“千呼万唤始出来”的 Swift 并发编程 犹如⼀剂强⼼针,出现在了⼤家⾯前。当⼴⼤ Swift 开发者们还沉浸 在终于得到了 async 和 await 的欢喜之时,我们不禁要想,对⽐起⼀ 些同级别的语⾔,这⼀切似乎有些姗姗来迟:并发和异步编程的前辈 语⾔ C# 早在 2012 年就加⼊了异步⽅法和任务 API;隔壁同为主打客 ⼾端开发起家的 Kotin 在 2016 年的 1.1 版中引⼊了协程 (coroutines) 预 览;与 Swift 社区有着不解之缘的 Rust 开发者们,也从 2019 年开始就 可以⾃由地徜徉在异步编程的世界中了。 为什么 Swift 中语⾔及标准库级别的异步和并发编程开发进度相对缓 慢,以⾄于那么多少年都“等⽩了头”?Swift 的异步编程模型要如何使 ⽤,是不是⽆脑跟着编译器提⽰写代码就万事⼤吉?Swift 并发编程和 其他语⾔中类似的概念有什么异同,学习这些概念能对其他语⾔的使 ⽤有所帮助吗?我们要如何更新⾃⼰的理解和认知,来利⽤这些新特 性完善我们的代码和项⽬,并进⼀步精进⾃⼰的技艺呢?在这本书 中,我想要带着这些问题,对 Swift 的异步和并发编程做⼀些研究, 和⼤家⼀同学习和探索。 ⽬标读者 这本书⾯向的是已经⼊⻔ Swift 并⾄少可以理解 Swift 语法、熟悉标准 库使⽤的开发者。我们会从像是⽅法调⽤或线程调度等⼀些基本概念 开始,逐步揭⽰出 Swift 异步和并发编程世界的特性,同时讲解⼤部 分语⾔和标准库中的有关概念。但我们并不会逐⾏解释程序 (特别是 那些同步世界中的代码) 的语法和运⾏机制。如果您想要从零开始学 习 Swift,或者接触 Swift 的时间还很短,那么在阅读时有可能会有⼀ 些难以理解的地⽅。如果遇到这种情况,建议您可以先阅读 Apple 官
Page
7
⽅的 Swift 教程中的相关内容 (您也可以在这⾥找到这些⽂档的中⽂版 本)。 特别地,您可能会需要有⼀定的 Swift 实际开发经验,才能更透彻地 理解 Swift 并发相关的内容,⾄少您应该使⽤过像是回调 (callback), 代理 (delegate) 等模式处理基本的异步操作。如果您⽤ Swift 开发过实 际项⽬的话,这个要求肯定不成问题:相关的操作遍布在 Swift 标准 库和⽇常的开发框架 (Foundation, UIKit 等) 的各个地⽅。如果您 (不幸 地) 主要还在使⽤ Objective-C 进⾏⽇常开发,您也可以通过本书理解 Swift 在并发编程⽅⾯所具有的巨⼤优势,这些知识在今后您进⾏迁移 时必将成为助⼒。 作为本书的读者,可能您对 Swift 相关的其他话题也会有兴趣,欢迎 查看我们的其他书籍。您可以在 ObjC 中国的⽹站找到其他所有书籍 的信息,它们包含了 Swift 开发的各个⽅⾯,相信您⼀定能找到⾃⼰ 感兴趣的话题,并藉此构建出更加完整的 Swift 知识体系。 章节结构 本书分为五个部分,它们包括了⼀个综述部分,三个对 Swift 并发中 主要模块进⾏详细解释的部分,以及最后对并发底层模型和调度⽅⾯ 说明的部分。具体来说: ⾸先是对 Swift 异步和并发编程模型的综述,在这个部分中,我 们希望阐明要解决的问题,并对 Swift 异步和并发的整套⽅案在 宏观上进⾏⼀个概览。我们⾸先要釐清⼀些基本的概念,虽然 Apple 使⽤ Swift 并发 (Swift Concurrency) 这个名词来描述 Swift 5.5 中加⼊的这些特性,但并发这个词在计算机科学中具有更⼴阔 的意涵。我们会通过⼀个具体的例⼦,对相关概念进⾏解释,并 基于这个例⼦讲述最基本的 Swift 并发编程⽅式。如果你只需要 不求甚解地“按照编译器提⽰”去处理简单的并发⼯作,那阅读这 个部分⼤概就可以覆盖你的⽇常需求了。
Page
8
对于希望“知其然,更知其所以然”的读者,第⼆个部分会开始对 Swift 并发编程进⾏深⼊探索,包括研究它的设计思想和部分实现 机理。Swift 并发⼤致由三块主要内容构成:异步函数、结构化并 发、以及 actor 模型。另外还有像是异步序列 (async sequence)、 任务本地值 (task local value) 等⼩⼀些的概念。这些内容环环相 扣,⼀同构建了 Swift 的并发模型。在这部分中,我们⼒求对这 些概念进⾏详细阐明,并探求⼀些更深⼊的话题及细节。 最后是对 Swift 并发底层机制的“刨根问底”,包括协作式线程池 的调度⽅式、异步函数的线程模型和执⾏器相关的话题。我们不 会去纠结源码级别的实现细节,但是会揭⽰ Swift 并发的⼀些底 层思想,以及探索怎样才能让 Swift 并发机制和现有的代码协同 合作并保证它的性能优势。 章节之间是有顺序关系的,笔者推荐按照书籍顺序去阅读每个章节, ⽽不是跳过其中某些内容。Swift 异步和并发编程的模型具有很⾼的统 合性,它们环环相扣:语法的设计和标准库中 API 的设计相辅相成, 合⼒解决了⼀系列在 Apple 平台编程时原本⼗分困难的问题,同时对 外提供了相对优雅的 API 设计和强有⼒的编译期间保证。按照顺序阅 读的话,可以有利于层层递进,逐步了解 Swift 异步和并发模型的设 计思路。 准备⼯作 Swift 在 5.5 版本中加⼊了异步⽅法的语⾔特性和⼀系列并发相关的 API,所以想要实践本书内容,你⾄少需要搭载 Swift 5.5 的开发环 境。如果你使⽤的是 macOS,最简单的⽅式就是登⼊ Apple 开发者⽹ 站,并下载 Xcode 13 或者后续版本。如果你使⽤ Linux 系统,也可以 遵循 Swift 官⽅⽹站的说明,下载并安装对应版本的 Swift。 当你准备好后,可以通过运⾏ swift 命令来确认你的 Swift ⼯具链版本 号,⼀切正常的话,应该可以看到类似的输出。请确认 Swift 版本号 ⼤于等于 5.5:
Page
9
$ swift --version Apple Swift version 5.5 (swiftlang-xxxx clang-yyyy) 我们已经准备好开始进⼊到 Swift 并发的世界中了!
Page
10
Swift 并发初步 虽然可能你已经跃跃欲试,想要创建第⼀个 Swift 的并发程序,但是 “名不正则⾔不顺”。在实际进⼊代码之前,作为全书开头,我还是想 先对⼏个重要的相关概念进⾏说明。这样在今后本书中,当我们提起 Swift 异步和并发时,对具体它指代了什么内容,能够取得统⼀的认 识。本章后半部分,我们会实际着⼿写⼀些 Swift 并发代码,来描述整 套体系的基本构成和⼯作流程。 ⼀些基本概念 同步和异步 在我们说到线程的执⾏⽅式时,同步 (synchronous) 和异步 (asynchronous) 是这个话题中最基本的⼀组概念。同步操作意味着在操 作完成之前,运⾏这个操作的线程都将被占⽤,直到函数最终被抛出 或者返回。Swift 5.5 之前,所有的函数都是同步函数,我们简单地使 ⽤ func 关键字来声明这样⼀个同步函数: var results: [String] = [] func addAppending(_ value: String, to string: String) { results.append(value.appending(string)) } addAppending 是⼀个同步函数,在它返回之前,运⾏它的线程将⽆ 法执⾏其他操作,或者说它不能被⽤来运⾏其他函数,必须等待当前 函数执⾏完成后这个线程才能做其他事情。
Page
11
在 iOS 开发中,我们使⽤的 UI 开发框架,也就是 UIKit 或者 SwiftUI,不是线程安全的:对⽤⼾输⼊的处理和 UI 的绘制,必须在与 主线程绑定的 main runloop 中进⾏。假设我们希望⽤⼾界⾯以每秒 60 帧的速率运⾏,那么主线程中每两次绘制之间,所能允许的处理时间 最多只有 16 毫秒 (1 / 60s)。当主线程中要同步处理的其他操作耗时很 少时 (⽐如我们的 addAppending,可能耗时只有⼏⼗纳秒),这不会造 成什么问题。但是,如果这个同步操作耗时过⻓的话,主线程将被阻 塞。它不能接受⽤⼾输⼊,也⽆法向 GPU 提交请求去绘制新的 UI,这 将导致⽤⼾界⾯掉帧甚⾄卡死。这种“⻓耗时”的操作,其实是很常⻅ 的:⽐如从⽹络请求中获取数据,从磁盘加载⼀个⼤⽂件,或者进⾏ 某些⾮常复杂的加解密运算等。 下⾯的 loadSignature 从某个⽹络 URL 读取字符串:如果这个操作发 ⽣在主线程,且耗时超过 16ms (这是很可能发⽣的,因为通过握⼿协 议建⽴⽹络连接,以及接收数据,都是⼀系列复杂操作),那么主线程 将⽆法处理其他任何操作,UI 将不会刷新。 // 从⽹络读取⼀个字符串 func loadSignature() throws -> String? { // someURL 是远程 URL,⽐如 https://example.com let data = try Data(contentsOf: someURL) return String(data: data, encoding: .utf8) } loadSignature 最终的耗时超过 16 ms,对 UI 的刷新或操作的处理不 得不被延后。在⽤⼾观感上,将表现为掉帧或者整个界⾯卡住。这是 客⼾端开发中绝对需要避免的问题之⼀。 Swift 5.5 之前,要解决这个问题,最常⻅的做法是将耗时的同步操作 转换为异步操作:把实际⻓时间执⾏的任务放到另外的线程 (或者叫做
Page
12
后台线程) 运⾏,然后在操作结束时提供运⾏在主线程的回调,以供 UI 操作之⽤: func loadSignature( _ completion: @escaping (String?, Error?) -> Void ) { DispatchQueue.global().async { do { let d = try Data(contentsOf: someURL) DispatchQueue.main.async { completion(String(data: d, encoding: .utf8), nil) } } catch { DispatchQueue.main.async { completion(nil, error) } } } } DispatchQueue.global 负责将任务添加到全局后台派发队列。在底 层,GCD 库 (Grand Central Dispatch) 会进⾏线程调度,为实际耗时繁 重的 Data.init(contentsOf:) 分配合适的线程。耗时任务在主线程外进 ⾏处理,完成后再由 DispatchQueue.main 派发回主线程,并按照结 果调⽤ completion 回调⽅法。这样⼀来,主线程不再承担耗时任务, UI 刷新和⽤⼾事件处理可以得到保障。 异步操作虽然可以避免卡顿,但是使⽤起来存在不少问题,最主要包 括:
Page
13
错误处理隐藏在回调函数的参数中,⽆法⽤ throw 的⽅式明确地 告知并强制调⽤侧去进⾏错误处理。 对回调函数的调⽤没有编译器保证,开发者可能会忘记调⽤ completion,或者多次调⽤ completion。 通过 DispatchQueue 进⾏线程调度很快会使代码复杂化。特别是 如果线程调度的操作被隐藏在被调⽤的⽅法中的时候,不查看源 码的话,在 (调⽤侧的) 回调函数中,⼏乎⽆法确定代码当前运⾏ 的线程状态。 对于正在执⾏的任务,没有很好的取消机制。 除此之外,还有其他⼀些没有列举的问题。它们都可能成为我们程序 中潜在 bug 的温床,在之后关于异步函数的章节⾥,我们会再回顾这 个例⼦,并仔细探讨这些问题的细节。 需要进⾏说明的是,虽然我们将运⾏在后台线程加载数据的⾏为称为 异步操作,但是接受回调函数作为参数的 loadSignature(_:) ⽅法,其 本⾝依然是⼀个同步函数。这个⽅法在返回前仍旧会占据主线程,只 不过它现在的执⾏时间⾮常短,UI 相关的操作不再受影响。 Swift 5.5 之前,Swift 语⾔中并没有真正异步函数的概念,我们稍后会 看到使⽤ async 修饰的异步函数是如何简化上⾯的代码的。 串⾏和并⾏ 另外⼀组重要的概念是串⾏和并⾏。对于通过同步⽅法执⾏的同步操 作来说,这些操作⼀定是以串⾏⽅式在同⼀线程中发⽣的。“做完⼀件 事,然后再进⾏下⼀件事”,是最常⻅的、也是我们⼈类最容易理解的 代码执⾏⽅式: if let signature = try loadSignature() { addAppending(signature, to: "some data") } print(results)
Page
14
loadSignature,addAppending 和 print 被顺次调⽤,它们在同⼀线 程中按严格的先后顺序发⽣。这种执⾏⽅式,我们将它称为串⾏ (serial)。 同步⽅法执⾏的同步操作,是串⾏的充分但⾮必要条件。异步操作也 可能会以串⾏⽅式执⾏。假设除了 loadSignature(_:) 以外,我们还有 ⼀个从数据库⾥读取⼀系列数据的函数,它使⽤类似的⽅法,把具体 ⼯作放到其他线程异步执⾏: func loadFromDatabase( _ completion: @escaping ([String]?, Error?) -> Void ) { // ... } 如果我们先从数据库中读取数据,在完成后再使⽤ loadSignature 从 ⽹络获取签名,最后将签名附加到每⼀条数据库中取出的字符串上, 可以这么写: loadFromDatabase { (strings, error) in if let strings = strings { loadSignature { signature, error in if let signature = signature { strings.forEach { addAppending(signature, to: $0) } } else { print("Error") } } } else { print("Error.") } } 虽然这些操作是异步的,但是它们 (从数据库读取 [String],从⽹络下 载签名,最后将签名添加到每条数据中) 依然是串⾏的,加载签名必定
Page
15
发⽣在读取数据库完成之后,⽽最后的 addAppending 也必然发⽣在 loadSignature 之后: 虽然图中把 loadFromDatabase 和 loadSignature 画在了同⼀个 线程⾥,但事实上它们有可能是在不同线程执⾏的。不过在上⾯ 代码的情况下,它们的先后次序依然是严格不变的。 事实上,虽然最后的 addAppending 任务同时需要原始数据和签名才 能进⾏,但 loadFromDatabase 和 loadSignature 之间其实并没有依 赖关系。如果它们能够⼀起执⾏的话,我们的程序有很⼤机率能变得 更快。这时候,我们会需要更多的线程,来同时执⾏两个操作: // loadFromDatabase { (strings, error) in // ... // loadSignature { signature, error in { // ... // 可以将串⾏调⽤替换为: loadFromDatabase { (strings, error) in //... } loadSignature { signature, error in //... } 为了确保在 addAppending 执⾏时,从数据库加载的内容和从⽹ 络下载的签名都已经准备好,我们需要某种⼿段来确保这些数据 的可⽤性。在 GCD 中,通常可以使⽤ DispatchGroup 或者 DispatchSemaphore 来实现这⼀点。但是我们并不是⼀本探讨 GCD 的书籍,所以这部分内容就略过了。
Page
16
两个 load ⽅法同时开始⼯作,理论上资源充⾜的话 (⾜够的 CPU,⽹ 络带宽等),现在它们所消耗的时间会⼩于串⾏时的两者之和: 这时候,loadFromDatabase 和 loadSignature 这两个异步操作,在 不同的线程中同时执⾏。对于这种拥有多套资源同时执⾏的⽅式,我 们就将它称为并⾏ (parallel)。 Swift 并发是什么 在有了这些基本概念后,最后可以谈谈关于并发 (concurrency) 这个名 词了。在计算机科学中,并发指的是多个计算同时执⾏的特性。并发 计算中涉及的同时执⾏,主要是若⼲个操作的开始和结束时间之间存 在重叠。它并不关⼼具体的执⾏⽅式:我们可以把同⼀个线程中的多 个操作交替运⾏ (这需要这类操作能够暂时被置于暂停状态) 叫做并 发,这⼏个操作将会是分时运⾏的;我们也可以把在不同处理器核⼼ 中运⾏的任务叫做并发,此时这些任务必定是并⾏的。 ⽽当 Apple 在定义“Swift 并发”是什么的时候,和上⾯这个经典的计算 机科学中的定义实质上没有太多不同。Swift 官⽅⽂档给出了这样的解 释: Swift 提供内建的⽀持,让开发者能以结构化的⽅式书写异步和并 ⾏的代码,… 并发这个术语,指的是异步和并⾏这⼀常⻅组合。
Page
17
所以在提到 Swift 并发时,它指的就是异步和并⾏代码的组合。这在语 义上,其实是传统并发的⼀个⼦集:它限制了实现并发的⼿段就是异 步代码,这个限定降低了我们理解并发的难度。在本书中,如果没有 特别说明,我们在提到 Swift 并发时,指的都是“异步和并⾏代码的组 合”这个简化版的意义,或者专指 Swift 5.5 中引⼊的这⼀套处理并发的 语法和框架。 除了定义⽅式稍有不同之外,Swift 并发和其他编程语⾔在处理同样问 题时所⾯临的挑战⼏乎⼀样。从戴克斯特拉 (Edsger W. Dijkstra) 提出信 号量 (semaphore) 的概念起,到东尼·霍尔爵⼠ (Tony Hoare) 使⽤ CSP 描述和尝试解决哲学家就餐问题,再到 actor 模型或者通道模型 (channel model) 的提出,并发编程最⼤的困难,以及这些⼯具所要解决 的问题⼤致上只有两个: 1. 如何确保不同运算运⾏步骤之间的交互或通信可以按照正确的顺 序执⾏ 2. 如何确保运算资源在不同运算之间被安全地共享、访问和传递 第⼀个问题负责并发的逻辑正确,第⼆个问题负责并发的内存安全。 在以前,开发者在使⽤ GCD 编写并发代码时往往需要很多经验,否则 难以正确处理上述问题。Swift 5.5 设计了异步函数的书写⽅法,在此 基础上,利⽤结构化并发确保运算步骤的交互和通信正确,利⽤ actor 模型确保共享的计算资源能在隔离的情况下被正确访问和操作。它们 组合在⼀起,提供了⼀系列⼯具让开发者能简单地编写出稳定⾼效的 并发代码。我们接下来,会浅显地对这⼏部分内容进⾏瞥视,并在后 ⾯对各个话题展开探究。 戴克斯特拉还发表了著名的《GOTO 语句有害论》(Go To Statement Considered Harmful),并和霍尔爵⼠⼀同推动了结构化编 程的发展。霍尔爵⼠在稍后也提出了对 null 的反对,最终促成了 现代语⾔中普遍采⽤的 Optional (或者叫别的名称,⽐如 Maybe 或 null safety 等) 设计。如果没有他们,也许我们今天在编写代码 时还在处理⽆尽的 goto 和 null 检查,会要⾟苦很多。
Page
18
异步函数 为了更容易和优雅地解决上⾯两个问题,Swift 需要在语⾔层⾯引⼊新 的⼯具:第⼀步就是添加异步函数的概念。在函数声明的返回箭头前 ⾯,加上 async 关键字,就可以把⼀个函数声明为异步函数: func loadSignature() async throws -> String { fatalError("暂未实现") } 异步函数的 async 关键字会帮助编译器确保两件事情: 1. 它允许我们在函数体内部使⽤ await 关键字; 2. 它要求其他⼈在调⽤这个函数时,使⽤ await 关键字。 这和与它处于类似位置的 throws 关键字有点相似。在使⽤ throws 时,它允许我们在函数内部使⽤ throw 抛出错误,并要求调⽤者使⽤ try 来处理可能的抛出。async 也扮演了这样⼀个⻆⾊,它要求在特定 情况下对当前函数进⾏标记,这是对于开发者的⼀种明确的提⽰,表 明这个函数有⼀些特别的性质:try/throw 代表了函数可以被抛出,⽽ await 则代表了函数在此处可能会放弃当前线程,它是程序的潜在暂停 点。 放弃线程的能⼒,意味着异步⽅法可以被“暂停”,这个线程可以被⽤ 来执⾏其他代码。如果这个线程是主线程的话,那么界⾯将不会卡 顿。被 await 的语句将被底层机制分配到其他合适的线程,在执⾏完 成后,之前的“暂停”将结束,异步⽅法从刚才的 await 语句后开始, 继续向下执⾏。 关于异步函数的设计和更多深⼊内容,我们会在随后的相关章节展 开。在这⾥,我们先来看看⼀个简单的异步函数的使⽤。Foundation 框 架中已经为我们提供了很多异步函数,⽐如使⽤ URLSession 从某个 URL 加载数据,现在也有异步版本了。在由 async 标记的异步函数 中,我们可以调⽤其他异步函数:
Page
19
func loadSignature() async throws -> String? { let (data, _) = try await URLSession.shared.data(from: someURL) return String(data: data, encoding: .utf8) } 这些 Foundation,或者 AppKit 或 UIKit 中的异步函数,有⼀部分 是重写和新添加的,但更多的情况是由相应的 Objective-C 接⼝转 换⽽来。满⾜⼀定条件的 Objective-C 函数,可以直接转换为 Swift 的异步函数,⾮常⽅便。在后⼀章我们也会具体谈到。 如果我们把 loadFromDatabase 也写成异步函数的形式。那么,在上 ⾯串⾏部分,原本的嵌套式的异步操作代码: loadFromDatabase { (strings, error) in if let strings = strings { loadSignature { signature, error in if let signature = signature { strings.forEach { addAppending(signature, to: $0) } } else { print("Error") } } } else { print("Error.") } } 就可以⾮常简单地写成这样的形式: let strings = try await loadFromDatabase() if let signature = try await loadSignature() { strings.forEach { addAppending(signature, to: $0) } } else { throw NoSignatureError() } 不⽤多说,单从代码⾏数就可以⼀眼看清优劣了。异步函数极⼤简化 了异步操作的写法,它避免了内嵌的回调,将异步操作按照顺序写成 了类似“同步执⾏”的⽅法。另外,这种写法允许我们使⽤ try/throw 的
Page
20
组合对错误进⾏处理,编译器会对所有的返回路径给出保证,⽽不必 像回调那样时刻检查是不是所有的路径都进⾏了处理。 结构化并发 对于同步函数来说,线程决定了它的执⾏环境。⽽对于异步函数,则 由任务 (Task) 决定执⾏环境。Swift 提供了⼀系列 Task 相关 API 来让 开发者创建、组织、检查和取消任务。这些 API 围绕着 Task 这⼀核⼼ 类型,为每⼀组并发任务构建出⼀棵结构化的任务树: ⼀个任务具有它⾃⼰的优先级和取消标识,它可以拥有若⼲个⼦ 任务并在其中执⾏异步函数。 当⼀个⽗任务被取消时,这个⽗任务的取消标识将被设置,并向 下传递到所有的⼦任务中去。 ⽆论是正常完成还是抛出错误,⼦任务会将结果向上报告给⽗任 务,在所有⼦任务完成之前 (不论是正常结束还是抛出),⽗任务 是不会完成的。 这些特性看上去和 Operation 类 有⼀些相似,不过 Task 直接利⽤异步 函数的语法,可以⽤更简洁的⽅式进⾏表达。⽽ Operation 则需要依 靠⼦类或者闭包。 在调⽤异步函数时,需要在它前⾯添加 await 关键字;⽽另⼀⽅⾯, 只有在异步函数中,我们才能使⽤ await 关键字。那么问题在于,第 ⼀个异步函数执⾏的上下⽂,或者说任务树的根节点,是怎么来的? 简单地使⽤ Task.init 就可以让我们获取⼀个任务执⾏的上下⽂环境, 它接受⼀个 async 标记的闭包: struct Task<Success, Failure> where Failure : Error { init( priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success
Comments 0
Loading comments...
Reply to Comment
Edit Comment