Swift Concurrency学习

1. Async/Await初识

在swift5.5的版本中 swift对 async/await关键字进行支持。而对于async/await关键字的作用,我们可以从下面的例子中开始。

swift 复制代码
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处理,调用如下

csharp 复制代码
/// 闭包回调方式
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中 系统和三方的库中为我们提供了很多异步函数,接下来我们需要自己定义一个异步方法

csharp 复制代码
func test() async -> Int {
  return 10
}

我们定义上述方法,但它真是一个异步方法吗? async关键字并不会真正的带来异步,异步的能力需要经过其他的处理。在常规的方法调用中,通过DispatchQueue 调度到其他线程,使得onComplete的回调脱离test1之前的调用栈。

less 复制代码
func test1(onComplete: @escaping (Int) -> Void) {
  DispatchQueue.global().async {
    onComplete(5)
  }
}

同理使用关键字async将同步方法变为异步,也需要类似的回调将结果传递出去。在swift中 采用了一种叫做Continuation Passing Style的设计思路,其中的Continuation就充当了回调的作用,swift标准库中的定义如下

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将获取的结果通过其传递过去,

swift 复制代码
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
javascript 复制代码
// 异常错误抛出 多了 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的构造器如下:

less 复制代码
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实例并运行这个闭包,在这个闭包中我们可以调用任意的异步函数.调用如下

csharp 复制代码
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的优先级,即如下调用,

swift 复制代码
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 {
        
      }
  }
​

TaskTask.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,对任务进行取消

swift 复制代码
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提供的一个机制,用于在同一个父任务中同时创建多个子任务并行运行,然后等待所有子任务完成。

csharp 复制代码
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要求任务结果类型要保持一致,当我们的业务涉及多个异步任务,但是每个异步任务的返回值类型都不同时,我们可以借助枚举来进行处理

csharp 复制代码
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

swift 复制代码
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 第一个错误,根据上面的写法不同,输出的结果也不一样。如果我们希望当一个子任务执行抛出异常时,取消所有子任务。可以在内部进行处理

csharp 复制代码
// 写法一
......................................................
/// ✔ 关键代码: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会并发执行所有任务,如果想要限制并发数,需要自己控制逻辑。

vbnet 复制代码
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实例不要泄漏到外部

csharp 复制代码
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的实例泄漏到外面没有任何的意义。

swift 复制代码
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进行操作

csharp 复制代码
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的结果返回顺序是不确定的)。

通过下面的两个示例,我们可以对比一下

swift 复制代码
/// 定义最后的返回模型
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进行处理时:

swift 复制代码
/// 网络接口返回
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标记为取消状态。

swift 复制代码
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()
}

上面的内容输出:

arduino 复制代码
"开始执行内容==="
"任务取消了==="
"任务执行完成,isCancelLed:true"

可以看到上面的log中 取消了Task, 但是Task后面的任务依然开始执行了. 这就说明 Task的取消只是一个状态标记,它不会强制Task的执行体中断,换句话说Task的取消并不像杀进程那样粗暴。而在Task体中 可以根据取消状态来判断后续是否要执行。

简单设置Task体中执行的逻辑如下: 可以看到取消之后 Task体中之后的任务将不再执行

swift 复制代码
let task = Task {
    debugPrint("开始执行内容===")
    try? await Task.sleep(for: .seconds(5))
    if !Task.isCancelled {
        debugPrint("任务执行完成,isCancelLed:(Task.isCancelled)")
    }
}
​
// 控制台输出
"开始执行内容==="
"任务取消了==="

实际上我们有个更方便的写法:Task.checkCancellation(),调用该方法会直接抛出一个异常,后面Task中的逻辑不再执行

swift 复制代码
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做一个包装:

swift 复制代码
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内的异步任务做一个封装, 并实现对取消的响应:

swift 复制代码
  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函数的回调。

less 复制代码
public func withTaskCancellationHandler<T>(
    operation: () async throws -> T, 
    onCancel handler: @Sendable () -> Void
) async rethrows -> T
  • operation: 当前Task中 执行的代码逻辑
  • onCancel: 在operation执行时,如果Task被取消,该回调立即执行

通过withTaskCancellationHandler函数,可以在调用第三方异步操作时,及时感知到Task的取消状态,并通知第三方取消异步操作。

9. TaskGroup的取消

vbnet 复制代码
  /// 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的逻辑(包含状态修改、访问等) 执行所在的的执行器

看下面的简单例子:

swift 复制代码
actor BankAccount {
   let accountNumber: Int 
   var balance: Double
   
   init(accountNumber: Int, initialDeposit: Double) {
      self.accountNumber = accountNumber
      self.balance = initialDeposit
   }
}
  • accountNumber 可以直接访问,因为其不可变,不可变意味着不存在线程安全的问题
  • 对可变状态balance的访问以及对函数deposit的调用都是异步调用,需要使用await。

基于上面的例子看下账户转账的内容:

swift 复制代码
/// 用户转账
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 复制代码
swift 提供了actor-isolated paramters这样的特性,字面意思即满足actor状态隔离的参数,如果我们在定义外部函数时,将需要访问的actor类型的参数声明为isolated,那么我们就可以在函数内部修改这个actor的状态,将deposit函数变为顶级函数, 使用isolated关键字进行修饰。
swift 复制代码
// 修改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来异步访问

swift 复制代码
extension BankAccount: CustomStringConvertible {
    /// 声明description 属性不被隔离
    nonisolated var description: String {
        "Bank Account#(accountNumber)"
    }
}

参考文档

闲话 Swift 协程

相关推荐
东坡肘子2 天前
OpenClaw 不错,但我好像没有那么需要 -- 肘子的 Swift 周报 #125
人工智能·swiftui·swift
Swift社区7 天前
LeetCode 391 完美矩形 - Swift 题解
算法·leetcode·swift
升讯威在线客服系统8 天前
从 GC 抖动到稳定低延迟:在升讯威客服系统中实践 Span 与 Memory 的高性能优化
java·javascript·python·算法·性能优化·php·swift
Swift社区8 天前
LeetCode 390 消除游戏 - Swift 题解
leetcode·游戏·swift
东坡肘子9 天前
春晚、机器人、AI 与 LLM -- 肘子的 Swift 周报 #124
人工智能·swiftui·swift
BatmanWayne12 天前
swift-微调补充
人工智能·swift
疯笔码良16 天前
【swiftUI】实现自定义的底部TabBar组件
ios·swiftui·swift
东坡肘子17 天前
祝大家马年新春快乐! -- 肘子的 Swift 周报 #123
人工智能·swiftui·swift
BatmanWayne17 天前
swift微调记录
微调·swift