1. Async/Await初识
在swift5.5的版本中 swift对 async/await关键字进行支持。而对于async/await关键字的作用,我们可以从下面的例子中开始。
import Combine
struct MyAsyncAwaitTest {
static func getImageData(with url: String) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: URL(string: "")!)
return data
}
}
上述定义的方法 通过url,发起一个网络请求,返回对应的数据,同时将可能出现的错误进行throws处理,调用如下
/// 闭包回调方式
getImageData1(url: "") { data in
// 请求成功处理
saveImage(onSuccess:{
// 其他操作
}, onError:{
})
} onError: { error in
// 错误处理
}
/// 使用async和await的方式
Task {
do {
let data = try await getImageData(with: "")
// 处理图片===
saveImage()
// 其他操作
} catch {
debugPrint("请求图片出错==(error.localizedDescription)")
}
}
上面方法中使用async/await关键字将异步的操作变为一个同步的操作,直接拿到网络请求值进行处理,对比之前的方法,使用多个闭包进行嵌套,在代码结构上更易读、更清晰,同时也能避免不必要的内存泄漏(class中)。
编程语言通过async关键字将函数分为两类,过去的普通函数为同步函数,被async关键字修饰的函数则为异步函数,调用异步函数的时候需要使用await关键字,使得异步调用拥有了挂起等恢复的语义。
2. 异步回调转为异步函数
swift中 系统和三方的库中为我们提供了很多异步函数,接下来我们需要自己定义一个异步方法
func test() async -> Int {
return 10
}
我们定义上述方法,但它真是一个异步方法吗? async关键字并不会真正的带来异步,异步的能力需要经过其他的处理。在常规的方法调用中,通过DispatchQueue 调度到其他线程,使得onComplete的回调脱离test1之前的调用栈。
func test1(onComplete: @escaping (Int) -> Void) {
DispatchQueue.global().async {
onComplete(5)
}
}
同理使用关键字async将同步方法变为异步,也需要类似的回调将结果传递出去。在swift中 采用了一种叫做Continuation Passing Style的设计思路,其中的Continuation就充当了回调的作用,swift标准库中的定义如下
@frozen public struct UnsafeContinuation<T, E> where E : Error {
public func resume(returning value: T) where E == Never
public func resume(returning value: T)
public func resume(throwing error: E)
}
上面有两种类型的函数, 一种是returning,一种是throwing,也就是任何一段代码,其执行的结果无非返回对应的结果或者抛出异常。Continuation其实就是描述协程当中异步代码在挂起点的状态,而程序需要恢复执行时,调用对应的resume函数即可。现在有Continuation了,我们需要通过Continuation将获取的结果通过其传递过去,
public func withCheckedContinuation<T> (function: String = #function, _ body: (CheckedContinuation<T, Never>) -> Void) async -> T
public func withCheckedThrowingContinuation<T>(function: String = #function, _ body: (CheckedContinuation<T, Never> -> Void) async -> T
接下来 我们使用上面的方法将函数返回值变为异步返回, 异步函数分为两种
- 有异常抛出: 函数名中 使用 async throws 关键字修饰,使用try await关键字进行调用
withCheckedThrowingContinuation - 无异常抛出: 函数中使用async 关键字修饰,使用await关键字进行调用
withCheckedContinuation
// 异常错误抛出 多了 throws 关键字
func helloAsync() async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if true {
continuation.resume(returning: 13)
} else {
continuation.resume(throwing: NSError(domain: "-1", code: 0))
}
}
}
}
// 无异常抛出
func helloAsync() async -> Int {
await withCheckedContinuation { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
continuation.resume(returning: 13)
}
}
}
上面就是将接口的异步回调转出异步函数。
3. 程序中调用异步函数
在上面的操作中,我们已经将异步回调的结果转为了异步函数。但是在普通函数中不能调用异步函数,定义好的异步函数该怎么调用呢?答案是 使用Task
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实例并运行这个闭包,在这个闭包中我们可以调用任意的异步函数.调用如下
static func getImageData() {
Task {
let result = try await helloAsync()
debugPrint("当前设置")
}
}
接下里我们看下Task中的一些详细的用法
1. Task初始构造
Swift 定义了 6 种标准优先级,对应关系如下(从高到低):
| 优先级 | 说明 | 典型使用场景 |
|---|---|---|
🟥 .high | 非常高的优先级(极少用) | 系统关键任务、自定义实时操作 |
🟧 .userInitiated | 用户主动触发、需立即结果 | 用户点击按钮后立即需要返回结果(例如加载视频、打开文档) |
🟨 .medium | 默认值(大多数 Task) | 一般异步逻辑、默认 async/await 调用 |
🟩 .utility | 可见但不紧急的任务 | 后台同步、缓存、进度条更新 |
🟦 .low | 优先级较低 | 延迟任务、非关键的后台逻辑 |
⬛️ .background | 最低优先级 | 后台清理、日志上传、缓存过期清除等 |
默认情况下不直接设置Task的优先级,即如下调用,
static func testPriorityLog() {
// 用户界面响应 - userInitiated
Task(priority: .userInitiated) {
debugPrint("当前priority: (Task.currentPriority),threat:(Thread.current)")
}
// 重要数据处理 - high
Task(priority: .high) {
debugPrint("当前priority: (Task.currentPriority),threat:(Thread.current)")
}
// 普通操作 - medium (默认)
Task(priority: .medium) {
debugPrint("当前priority: (Task.currentPriority),threat:(Thread.current)")
}
// 后台同步 - utility
Task(priority: .utility) {
debugPrint("当前priority: (Task.currentPriority),threat:(Thread.current)")
}
// 清理工作 - background
Task(priority: .background) {
debugPrint("当前priority: (Task.currentPriority),threat:(Thread.current)")
}
// Task {} 内部再创建 Task {}(未显式设置) ✅ 继承父任务优先级
Task {
debugPrint("当前priority 父1: (Task.currentPriority),threat:(Thread.current)")
Task {
debugPrint("当前priority 子1: (Task.currentPriority),threat:(Thread.current)")
}
}
// Task {} 内部再创建 Task(priority: …) ❌ 不会继承(使用显式指定的优先级)
Task(priority: .medium) {
debugPrint("当前priority 父2: (Task.currentPriority),threat:(Thread.current)")
Task(priority: .userInitiated) {
debugPrint("当前priority 子2: (Task.currentPriority),threat:(Thread.current)")
}
}
// Task.detached {} ❌ 永远不继承,默认 .medium 除非手动设置
Task(priority: .medium) {
debugPrint("当前priority 父3: (Task.currentPriority),threat:(Thread.current)")
Task.detached {
debugPrint("当前priority 子3: (Task.currentPriority),threat:(Thread.current)")
}
}
// 指定在主线程中执行, 优先级设置的是high
//main: TaskPriority.high,threat:<_NSMainThread: 0x10462fdd0>{number = 1, name = main}"
Task {@MainActor in
debugPrint("当前priority main: (Task.currentPriority),threat:(Thread.current)")
}
// 表示立即执行,而不是放到线程池中等待调度
Task.immediate {
}
}
Task,Task.detached以及Task.immediate 的区别如下
| 特性 | Task {} | Task.detached {} | Task.immediate {} |
|---|---|---|---|
| 是否继承上下文(Actor、优先级、任务取消等) | ✅ 会继承父 Task 的上下文 | ❌ 完全独立 | ✅(执行时立即,仍在当前上下文) |
| 调度方式 | 异步调度(通过全局执行器/actor) | 异步调度(独立执行) | 同步立即执行(当前线程) |
| 执行线程 | 不确定(由系统决定) | 不确定(独立线程池) | 当前线程 |
| 执行时机 | 延迟调度(异步) | 延迟调度(异步) | 立即执行(同步) |
| 是否在 Actor 隔离内执行 | ✅ 保留 actor 隔离(如 @MainActor) | ❌ 无隔离,运行在全局 | ✅ 保留当前 actor 隔离 |
| 优先级继承 | ✅ | ❌ | ✅ |
| 是否可取消 | ✅(继承父任务取消) | ✅(但不受父任务影响) | ✅(同步时意义不大) |
| 使用场景 | 一般异步操作 | 完全独立后台任务 | 性能敏感 / 立即执行逻辑 |
| 调度成本 | 较低(异步调度) | 较高(独立调度) | 最低(直接执行) |
Task {}:跟我一起干(继承上下文)Task.detached {}:我自己干(独立执行)Task.immediate {}: 立即执行,不放到线程池中等待调度
2. Task暂停
Task中 提供一个api ==> yield() 表示暂停当前任务的执行,将执行机会让给调度器(task scheduler) , 让其他等待执行的任务有机会运行
Task.sleep() 只是让当前task进行休眠,定时器的作用,延时操作。对yield和sleep做个对比
| 对比项 | Task.yield() | Task.sleep() |
|---|---|---|
| 行为 | 主动让出执行权 | 休眠一段时间 |
| 延迟 | 几乎为 0 | 指定时间(如 1s) |
| 场景 | 任务调度、公平性、可取消 | 定时器、延时操作 |
| 可被取消 | ✅ | ✅ |
3. Task 取消
定义对应的Task,返回值是一个task,在视图消失或业务需求场景中 需要主动调用cancel,对任务进行取消
let parent = Task {
Task {
try await Task.sleep(nanoseconds: 1_000_000_000)
print("子任务结束")
}
try await Task.sleep(nanoseconds: 500_000_000)
print("父任务取消")
}
parent.cancel()
// 父任务取消
//(子任务被一同取消,不会执行)
let parent1 = Task {
Task.detached {
try await Task.sleep(nanoseconds: 1_000_000_000)
print("Detached 子任务完成 ✅")
}
try await Task.sleep(nanoseconds: 500_000_000)
print("父任务取消")
}
parent1.cancel()
// 父任务取消
// Detached 子任务完成 ✅
另外在swiftUI中使用.task创建的任务,会在View销毁的时候自动调用 task.cancel()。
4. TaskGroup使用
TaskGroup 是 swift Concurrency提供的一个机制,用于在同一个父任务中同时创建多个子任务并行运行,然后等待所有子任务完成。
func fetchAllData() async {
await withTaskGroup(of: String.self) { group in
group.addTask {
await fetchUserName()
}
group.addTask {
await fetchUserAvatar()
}
group.addTask {
await fetchUserBio()
}
for await result in group {
// 任务完成
print("完成任务:", result)
}
print("全部任务完成")
}
}
- withTaskGroup(of: T.self): 表示组中所有任务返回值类型为T
- addTask 添加子任务
- for await result in group: 逐步获取子任务的结果(完成一个返回一个)
使用Group做结构化并发时的特点
| 特性 | 描述 |
|---|---|
| ✅ 结构化 | 所有子任务都“属于”父任务,自动在作用域结束时取消/等待 |
| ✅ 安全 | 无需手动同步、join |
| ✅ 类型安全 | 所有任务结果类型一致 |
| ✅ 自动取消传播 | 父任务取消时,所有子任务一起取消 |
由于TaskGroup要求任务结果类型要保持一致,当我们的业务涉及多个异步任务,但是每个异步任务的返回值类型都不同时,我们可以借助枚举来进行处理
enum APIResult {
case user(UserInfo)
case video(VideoInfo)
case order(OrderInfo)
}
/// 定义请求方法,返回值类型 APIResult,枚举中包含参数 返回子任务真正返回的数据类型
func loadAll() async {
await withTaskGroup(of: APIResult.self) { group in
group.addTask {
let user = try await fetchUserInfo()
return .user(user)
}
group.addTask {
let video = try await fetchVideoInfo()
return .video(video)
}
group.addTask {
let order = try await fetchOrderInfo()
return .order(order)
}
for await result in group {
switch result {
case .user(let user):
print("User:", user)
case .video(let video):
print("Video:", video)
case .order(let order):
print("Order:", order)
}
}
}
}
TaskGroup 还有带错误处理的版本 withThrowingTaskGroup
static func testThrowingTask() async throws -> Int {
let result = try await withThrowingTaskGroup(of: Int.self) { group -> Int in
group.addTask {
try await Task.sleep(for: .seconds(5))
debugPrint("开始执行 time: 5")
return -1
}
group.addTask {
try await Task.sleep(for: .seconds(8))
debugPrint("开始执行 time: 8")
throw NSError(domain: "-1", code: -1)
}
group.addTask {
try await Task.sleep(for: .seconds(10))
debugPrint("开始执行 time: 10")
return 1
}
/// 写法一: Task.sleep(for: .seconds(10)) 之后不会输出
for try await data in group {
debugPrint("执行结果输出 data:(data)")
}
/*
上面这种写法中:第二个任务会抛出一个异常,group.next()抛出错误,for try await会立即终止循环,错误将被抛到外层进行处理, 直接退出,其他任务依然会执行,但是 看不到输出的结果,循环已经推出,之后也没有办法从group获取剩下的结果(group已经终止)
*/
/// 写法二: Task.sleep(for: .seconds(10)) 会输出,result会返回,整体没有错误对外抛出,内部已经处理错误了
while(!group.isEmpty) {
do {
print(try await group.next() ?? "Nil")
} catch {
print(error)
}
}
/*
group.next()每次返回一个子任务的结果或throwable error,内部用catch把错误进行了处理, group不会提前退出 ,任务还会继续,循环还会继续下去,可以看到所有任务执行完毕,并且有返回值。
*/
return 100
}
return result
}
withThrowingTaskGroup中 某个子任务抛出错误时, 整个组会throw 第一个错误,根据上面的写法不同,输出的结果也不一样。如果我们希望当一个子任务执行抛出异常时,取消所有子任务。可以在内部进行处理
// 写法一
………………………………………………
/// ✔ 关键代码:for-in + 自动取消
do {
for try await value in group {
print("结果: (value)")
}
} catch {
/// 一旦发现错误 → 手动取消所有任务
group.cancelAll()
print("检测到错误,已取消所有任务")
throw error // 向外抛出错误
}
………………………………………………
// 写法二
…………………………………………
while(!group.isEmpty) {
do {
print(try await group.next() ?? "Nil")
} catch {
print(error)
// 取消所有的任务
group.cancelAll()
}
}
………………………………………………
| 能力 | TaskGroup | withThrowingTaskGroup |
|---|---|---|
是否能 throw | ❌ | ✅ |
| 子任务异常传播 | ❌ 忽略 | ✅ 自动传播 |
| 取消传播 | ✅ 自动 | ✅ 自动 |
| 返回值 | ✅ 可聚合 | ✅ 可聚合 |
| 用途 | 并行任务集合 | 带错误处理的并行任务集合 |
4. TaskGroup的并发限制
默认情况下,TaskGroup会并发执行所有任务,如果想要限制并发数,需要自己控制逻辑。
func fetchManyLimited() async {
let items = Array(1...100)
let limit = 5
await withTaskGroup(of: Int.self) { group in
var iterator = items.makeIterator()
// 先添加五个task
for _ in 0..<limit {
if let next = iterator.next() {
group.addTask { await helloAsync() }
}
}
// 有一个任务完成了,就把下一个任务添加到group中,知道数组中的数据迭代完成
for await _ in group {
if let next = iterator.next() {
group.addTask { await helloAsync() }
}
}
}
}
5. TaskGroup实例不要泄漏到外部
var taskGroup: TaskGroup<Int>?
_ = await withTaskGroup(of: Int.self) { (group) -> Int in
taskGroup = group
group.addTask { 1 }
return 0
}
guard let group = taskGroup else {
print("group is nil")
return
}
for await i in group {
print(i)
}
在外部访问TaskGroup后,直接报错。Process finished with exit code 133 (interrupted by signal 5: SIGTRAP). 而其原因是WithTaskGroup会在所有的子Task执行完成了之后再返回,通过swift源码我们可以看到, group返回前会先等待所有的子Task执行完毕,然后将TaskGroup销毁,因此将TaskGroup的实例泄漏到外面没有任何的意义。
public func withTaskGroup<ChildTaskResult, GroupResult>(
of childTaskResultType: ChildTaskResult.Type,
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult {
let _group = Builtin.createTaskGroup(ChildTaskResult.self)
var group = TaskGroup<ChildTaskResult>(group: _group)
// Run the withTaskGroup body.
let result = await body(&group)
await group.awaitAllRemainingTasks()
Builtin.destroyTaskGroup(_group)
return result
}
6. TaskGroup实例不要在子Task中进行修改
TaskGroup泄漏到外部是比较危险的,如果我们在子Task中对TaskGroup进行操作
await withTaskGroup(of: Void.self) { (group) -> Void in
group.addTask {
group.addTask { // error!
print("inner task")
}
}
}
运行会发现,执行立即报错
Mutation of captured parameter 'group' in concurrently-executing code, 子Task的执行可能会被调度到其他的线程上,导致对GroupTask的修改是并发的,并不是线程安全的,
7. 与 async let对比
swift中除了使用TaskGroup来构造结构化并发以外,还有一种更加简洁的方式,使用 async let. 使用async let一方面可以让子task的创建和结果的返回变得更加简单(不用统一返回类型),另一方面也可以解决子Task返回error 不好定位的问题(TaskGroup中的子Task的结果返回顺序是不确定的)。
通过下面的两个示例,我们可以对比一下
/// 定义最后的返回模型
struct User {
let name: String
let info: String
let followers: [String]
let projects: [String]
}
/// 网络接口返回
func getUserInfo(_ user: String) async -> String {………………}
func getFollowers(_ user: String) async -> [String] {………………}
func getProjects(_ user: String) async -> [String] {………………}
/// 接口返回结果处理
enum Result {
case info(value: String)
case followers(value: [String])
case projects(value: [String])
}
/// 构建结构化并发
func getUser(_ name: String) async -> User {
await withTaskGroup(of: Result.self) { group in
group.addTask {
.info(value: await getUserInfo(name))
}
group.addTask {
.followers(value: await getFollowers(name))
}
group.addTask {
.projects(value: await getProjects(name))
}
var info: String? = nil
var followers: [String]? = nil
var projects: [String]? = nil
for await r in group {
switch r {
case .info(value: let value):
info = value
case .followers(value: let value):
followers = value
case .projects(value: let value):
projects = value
}
}
return User(name: name, info: info ?? "", followers: followers ?? [], projects: projects ?? [])
}
}
因为子task返回的顺序是不确定的,我们只能通过for……in将返回的结果和Result对应的类型进行绑定,方便后续的读取和设置。多个taskGroup执行时,需要定义多个 返回模型和枚举值。在实现并发上更加的繁琐。
当使用async let进行处理时:
/// 网络接口返回
func getUserInfo(_ user: String) async -> String {………………}
func getFollowers(_ user: String) async -> [String] {………………}
func getProjects(_ user: String) async -> [String] {………………}
/// 获取用户信息
func getUser(name: String) async -> User {
async let info = getUserInfo(name)
async let followers = getFollowers(name)
async let projects = getProjects(name)
/// 三个任务都是异步的,并发执行的,
let (infoData, followersData, projectsData) = await (info, followers, projects)
return User(name: name, info: infoData, followers: followersData, projects: projectsData)
}
async let会创建一个子Task来完成后面的调用,并且会把结果绑定到对应的变量中。以info为例,当我们要获取结果时,只需要await info 即可,这样就打打降低了获取异步子Task结果的复杂度。
| 特性 | async let | TaskGroup |
|---|---|---|
| 用法 | 简洁语法声明少量并发任务 | 适合动态数量的任务 |
| 子任务数量 | 固定(编译时已知) | 动态(运行时添加) |
| 返回值 | 明确绑定变量 | 遍历 group 获取 |
| 是否结构化并发 | ✅ 是 | ✅ 是 |
| 适用场景 | 少量并行请求(如2~3个) | 大量并行或动态任务数 |
8. Task的取消
Task的取消其实非常简单,就是将Task标记为取消状态。
static func testCancelTask() async {
let task = Task {
debugPrint("开始执行内容===")
try? await Task.sleep(for: .seconds(5))
debugPrint("任务执行完成,isCancelLed:(Task.isCancelled)")
}
try? await Task.sleep(for: .seconds(1))
debugPrint("任务取消了===")
// 取消Task
task.cancel()
}
上面的内容输出:
"开始执行内容==="
"任务取消了==="
"任务执行完成,isCancelLed:true"
可以看到上面的log中 取消了Task, 但是Task后面的任务依然开始执行了. 这就说明 Task的取消只是一个状态标记,它不会强制Task的执行体中断,换句话说Task的取消并不像杀进程那样粗暴。而在Task体中 可以根据取消状态来判断后续是否要执行。
简单设置Task体中执行的逻辑如下: 可以看到取消之后 Task体中之后的任务将不再执行
let task = Task {
debugPrint("开始执行内容===")
try? await Task.sleep(for: .seconds(5))
if !Task.isCancelled {
debugPrint("任务执行完成,isCancelLed:(Task.isCancelled)")
}
}
// 控制台输出
"开始执行内容==="
"任务取消了==="
实际上我们有个更方便的写法:Task.checkCancellation(),调用该方法会直接抛出一个异常,后面Task中的逻辑不再执行
let task = Task {
debugPrint("开始执行内容===")
try? await Task.sleep(for: .seconds(5))
try Task.checkCancellation()
debugPrint("任务执行完成,isCancelLed:(Task.isCancelled)")
}
// 实际上 Task.checkCancellation()的实现非常直接, 源码判断取消了 直接抛个异常出来,终止后续逻辑
public static func checkCancellation() throws {
if Task<Never, Never>.isCancelled {
throw _Concurrency.CancellationError()
}
}
前面提到的响应取消包含两种场景:
- 调用其他支持响应取消的异步函数,在取消时它会抛出 CancellationError
- 自己的代码中主动检查取消状态,并抛出CancellationError(或 直接退出执行逻辑)
但是如果异步的逻辑在三方代码中进行了封装,我们只能在Task取消时调用三方的取消逻辑来处理,这种情形稍微复杂,我们以GCD的异步API为例。首先对DispatchWorkItem做一个包装:
class ContinuationWorkItem<T, E> where E: Error {
/// 继续要做的任务
var continuation: CheckedContinuation<T, E>?
/// 回到的block
let block: (ContinuationWorkItem) -> T
lazy var dispatchItem: DispatchWorkItem = DispatchWorkItem {
self.continuation?.resume(returning: self.block(self))
}
init(block: @escaping (ContinuationWorkItem<T, E>) -> T) {
self.block = block
}
/// 插入执行的内容
func installContinuation(continuation: CheckedContinuation<T, E>) {
self.continuation = continuation
}
func cancel() {
dispatchItem.cancel()
}
}
包装的目的在于支持installContinuation, 通过获取Task的Continuation来实现异步结果的返回。其中的block参数中比DispatchWorkItem的Block类型多了一个参数let block: (ContinuationWorkItem) -> T,主要是方便在block中读取到GCD的任务是否被取消了。
现在我们使用上面的封装对Task内的异步任务做一个封装, 并实现对取消的响应:
static func asyncContinuationTask() async {
let task = Task { () -> Int in
// block中 涉及的自己 是为了在异步任务中及时的获取当前任务的状态
let asyncRequest = ContinuationWorkItem<Int, Never> { item in
debugPrint("async start")
var i = 0
while i < 10 && !item.isCancelled {
Thread.sleep(forTimeInterval: 1.0)
i += 1
debugPrint("async task reult:(i)")
}
if item.isCancelled {
debugPrint("async Cancelled:(i)")
return 0
} else {
debugPrint("async Task Finish")
return 1
}
}
return await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
asyncRequest.installContinuation(continuation: continuation)
DispatchQueue.global().async(execute: asyncRequest.dispatchItem)
}
} onCancel: {
asyncRequest.cancel()
}
}
try? await Task.sleep(for: .seconds(2))
task.cancel()
debugPrint("task end (await task.result)")
}
asyncRequest其实就是我们创建的对ContinuationWorkItem的实例,它对DispatchWorkItem做了包装,在后面传给了DispatchQueue去异步执行。为了及时感知 Task的取消状态,我们使用 withTaskCancellationHandler函数的回调。
public func withTaskCancellationHandler<T>(
operation: () async throws -> T,
onCancel handler: @Sendable () -> Void
) async rethrows -> T
- operation: 当前Task中 执行的代码逻辑
- onCancel: 在operation执行时,如果Task被取消,该回调立即执行
通过withTaskCancellationHandler函数,可以在调用第三方异步操作时,及时感知到Task的取消状态,并通知第三方取消异步操作。
9. TaskGroup的取消
/// TaskGroup 取消
static func testTaskGroupCancel() async {
let max = 10
let taskCount = 10
await withTaskGroup(of: (Int, Int).self) { group -> Void in
for i in 0..<taskCount {
group.addTask {
var count = 0
while !Task.isCancelled && count < max {
try? await Task.sleep(for: .seconds(6))
count += 1
print("Task: (i), count: (count)")
}
return (i, count)
}
}
try? await Task.sleep(for: .seconds(5.5))
group.cancelAll()
for await result in group {
print("result: Task: (result.0), count:(result.1)")
}
}
}
// 执行结果如下(每次都是随机展示)
Task: 1, count: 1
Task: 0, count: 1
Task: 2, count: 1
Task: 4, count: 1
Task: 5, count: 1
Task: 8, count: 1
Task: 9, count: 1
Task: 3, count: 1
Task: 6, count: 1
Task: 7, count: 1
result: Task: 1, count:1
result: Task: 0, count:1
result: Task: 2, count:1
result: Task: 4, count:1
result: Task: 5, count:1
result: Task: 8, count:1
result: Task: 9, count:1
result: Task: 3, count:1
result: Task: 6, count:1
result: Task: 7, count:1
在上面的日志中,当Group调用cancel之后,所有的count数据都没有再增加,即所有的的Task都被取消了。
Actor的属性和隔离
1. 什么是actor
swift 为了解决线程安全问题,引入了一个非常有用的概念叫做actor。Actor模型是计算机科学领域的一个用于并行计算的数学模型,其中actor是模型当中的基本计算单元。
swift中, Actor包含state、mailbox、executor三个重要的组成部分,其中
- state 就是actor当中存储的值, 它是受到actor保护的,访问时会有一些限制以避免数据竞争(data race)
- mailbox: 字面意思就是邮箱的意思,这里就是理解为一个消息队列。外部对于actor的可变状态的方位需要发送一个异步消息到mailbox当中,actor的executor会串行的执行mailbox当中的消息以确保state是线程安全的
- executor:actor的逻辑(包含状态修改、访问等) 执行所在的的执行器
看下面的简单例子:
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}
- accountNumber 可以直接访问,因为其不可变,不可变意味着不存在线程安全的问题
- 对可变状态balance的访问以及对函数deposit的调用都是异步调用,需要使用await。
基于上面的例子看下账户转账的内容:
/// 用户转账
extension BankAccount {
enum BankError: Error {
case insufficientFunds
}
func transfer(amount: Double, to other: BankAccount) async throws {
assert(amount > 0)
if amount > balance {
// 余额不足
throw BankError.insufficientFunds
}
balance = balance - amount
// 其他用户 余额增加
await other.deposit(amount: amount)
}
}
函数transfer是BankAccount自己的函数,修改自己的balance自然没有什么问题,但是修改other的BankAccount实例的balance的值却是不行的,transfer函数执行时实际上是self这个实例在处理自己的额度,这里面如果偷偷修改了other的balance的值,就可能导致other的状态出现问题,
上面的例子中,actor的状态只能在自己的实例函数的内部进行修改,不能跨实例进行修改。
2. 外部函数修改actor的状态
前面说的是actor的状态只能在自己的函数内部修改,是因为actor的函数调用是在对应的executor上安全的执行,如果外部的函数也能够满足这个调用条件,那么理论上也是安全的。
swift 提供了actor-isolated paramters这样的特性,字面意思即满足actor状态隔离的参数,如果我们在定义外部函数时,将需要访问的actor类型的参数声明为isolated,那么我们就可以在函数内部修改这个actor的状态,将deposit函数变为顶级函数, 使用isolated关键字进行修饰。
// 修改deposit 函数 使其成为顶级函数,使用 isolated
func deposit(amount: Double, to account: isolated BankAccount) {
assert(amount > 0)
account.balance = account.balance + amount
}
注意到参数 account 的类型被关键字 isolated 修饰,表明函数 deposit 的调用需要保证 account 的状态修改安全。不难想到,对于这个函数的调用,我们需要使用 await:
3. 声明不需要隔离的属性或函数
actor中的属性 默认都是需要被隔离保护的,但也有一些属性可能并不需要被保护。需要我们提前进行声明,在属性前添加 nonisolated 关键字, nonisolated 同样也可以用来修饰函数,但这样的函数就不能直接访问被隔离的状态,只能像外部函数一样使用await来异步访问
extension BankAccount: CustomStringConvertible {
/// 声明description 属性不被隔离
nonisolated var description: String {
"Bank Account#(accountNumber)"
}
}