利用 Swift 并发功能提升应用性能

104 阅读12分钟

为什么学习Swift并发 在移动应用开发中,并发编程是提升用户体验的关键技能。掌握 Swift 并发,让我们的应用更流畅、更高效!

📚 学习路径概览

  • 🔰 初级知识
  • ⚡ 中级知识
  • 🚀 高级知识
  • 🎯 实战应用
  • 💡 常见陷阱

🔰 初级知识 - 打好基础

🎯 并发编程基础概念

并发 vs 并行

  • 并发(Concurrency) :程序能同时处理多个任务(任务切换,宏观同时)。
  • 并行(Parallelism) :多个任务在多个CPU核上真正同时执行。

并发就像餐厅服务员同时服务多桌客人(来回切换),并行就像多个服务员同时服务不同客人。

🤔 为什么需要并发?

  • ✅ 提高应用响应性(UI不卡顿)
  • ✅ 充分利用多核CPU
  • ✅ 提升资源利用率

🆕 Swift 并发模型简介

Swift 5.5 引入了协程(Coroutine)结构化 并发概念。主要依赖

  • async/await - 异步编程的核心
  • Task ****- 任务管理
  • Actor - 数据隔离

结构化任务使用async let或任务组创建,而非结构化任务则使用 TaskTask.detached 创建。结构化任务会像局部变量一样,一直存活到声明它的作用域结束为止,并且在超出作用域时会自动取消,从而明确了任务的存活时长。

⚡ async/await 语法体系

🔑 关键字与语法

  • async:声明异步函数
  • await:等待异步操作完成
  • throws/try:异步函数也可抛出错误
func fetchData() async throws -> Data {
    // 异步操作
    return Data()
}

func processData() async {
    do {
        let data = try await fetchData()
        // 处理数据
    } catch {
        print("错误:(error)")
    }
}

🎭 使用场景

网络请求、磁盘IO、耗时计算等

UI层调用异步数据加载

⚠️ 注意事项

  • ❌ 只能在异步上下文中使用 await
  • ❌ 同步函数不能直接调用异步函数
  • ✅ 异步函数可以调用同步函数

🔄 异步序列(AsyncSequence)

🎯 核心概念

  • AsyncSequenceAsyncIteratorProtocol

用于异步流式数据处理(如网络流、文件流)

struct Counter: AsyncSequence {
    let limit: Int
    struct AsyncIterator: AsyncIteratorProtocol {
        let limit: Int
        var current = 0
        mutating func next() async -> Int? {
            guard current < limit else { return nil }
            current += 1
            return current
        }
    }
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(limit: limit)
    }
}

这段代码定义了一个名为 Counter 的结构体,它实现了 Swift 的 AsyncSequence 协议。在使用时可以用 for await 来异步遍历。

AsyncIterator实现了 AsyncIteratorProtocol 协议,用于生成序列中的下一个元素。其中next()异步迭代器的核心方法,每次调用会返回下一个计数值,直到达到 limit。

使用方法

for await number in Counter(limit: 5) {
    print("计数:(number)")
}

🎬 典型场景

网络数据流、异步文件读取、实时数据处理

⚠️ 注意事项

  • for await只能在异步上下文中使用
  • ✅ 异步序列适合处理流式、分批到达的数据

⚡ 中级知识 - 进阶技能

🎯 Task 详解 - 异步任务的核心

🔑 基础语法与用法

  1. 创建异步任务Task { ... }
let task = Task {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    return "任务完成"
}

// 带返回值的任务
let dataTask = Task<String, Error> {
    let data = try await fetchDataFromNetwork()
    return data
}

Task { ... }一创建就开始执行

  1. 获取任务结果await task.value
let result = await task.value

// 处理可能抛出的错误
do {
    let data = try await dataTask.value
    print("获取到数据:(data)")
} catch {
    print("任务失败:(error)")
}

3. #### 取消任务 task.cancel()

// 取消任务
task.cancel()

// 检查任务是否被取消
if task.isCancelled {
    print("任务已被取消")
}

4. #### 检测取消状态 Task.isCancelled

let task = Task {
    // 在任务内部检查取消状态
    guard !Task.isCancelled else {
        print("任务被取消,提前退出")
        return
    }
    
    // 执行任务逻辑
    try await performHeavyWork()
}

关于取消任务和检查取消状态,这里再做一个深入的了解,因为这两个还是会经常被使用到的。

一个任务想要取消,只有两种操作:

  • 任务自身的 isCancel 设置为 true
  • 在结构化并发任务: 如果任务有子任务,对子任务调用 cancel

至于子任务是否 cancel,取决于子任务本身对 isCancel 的检查. 取消的方式可以通过

  • 检查 isCancel 的方式,返回正常兜底值
  • 执行 checkCanceled 的方式,抛出错误, 由顶层 Task 的创建者的所在块进行捕获.

举个复杂例子

import Foundation

func work(_ text: String) async throws -> String {
    var s = ""
    for c in text {
        // 读取父级Task状态
        if Task.isCancelled {
            print("Cancelled: (text)")
        }
        try Task.checkCancellation()
        await Task.sleep(UInt64(1 * NSEC_PER_SEC))
        print("Append: (c)")
        s.append(c)
    }
    print("Done: (s)")
    return s
}

@main
struct Test {
    static func main() async {
        do {
            let value: String = try await withThrowingTaskGroup(of: String.self) { group in
                // Task 1
                group.addTask {
                    try await withThrowingTaskGroup(of: String.self) { inner in
                        // Task 1.1
                        inner.addTask { try await work("Hello") }
                        // Task 1.2
                        inner.addTask { try await work("World!") }
                        // 延迟 2.5 秒后取消 inner
                        await Task.sleep(UInt64(2.5 * Double(NSEC_PER_SEC)))
                        inner.cancelAll()
                        return try await inner.reduce([]) { $0 + [$1] }.joined(separator: " ")
                    }
                }

                // Task 2
                group.addTask {
                    let result = try await work("Swift Concurrency")
                    return result
                }

                // 注意:reduce 在外层 group
                return try await group.reduce([]) { $0 + [$1] }.joined(separator: " ")
            }
            print("Final value: (value)")
        } catch {
            print("Error: (error)")
        }
    }
}
时间Task 1.1 (Hello)Task 1.2 (World!)Task 2 (Swift Concurrency)事件
0.0sAppend: H (耗时1s)Append: W (耗时1s)Append: S (耗时1s)三个任务同时开始
1.0sAppend: e (耗时1s)Append: o (耗时1s)Append: w (耗时1s)继续执行
2.0sAppend: l (耗时1s)Append: r (耗时1s)Append: i (耗时1s)即将触发取消
2.5sCancelled: Hello抛 CancellationError**Cancelled: World!**抛 CancellationError内层 group 调用 cancelAll()Task 1 的两个子任务终止
2.6sTask 1 捕获错误 → 向外抛Cancelled: Swift Concurrency抛 CancellationError外层 group 接收到错误Task 2 终止

screenshot-20250911-143223.png

知识点解析

Swift Concurrency 的取消是协作式的

  • 任务并不会立刻停下来
  • 必须在任务内部调用 Task.checkCancellation() 或访问 Task.isCancelled 时,才会检测到取消并抛出 CancellationError

取消是会向下传播的

  • inner.cancelAll() 会取消内层任务组里的全部任务
  • 内层任务组如果抛错,外层任务组也会收到错误并自动取消剩余的任务

外层任务组出错 → 取消其他任务

  • Task 1 出错后,外层 group 会取消 Task 2
  • Task 2 在下一次 checkCancellation() 时退出
  1. 设置优先级Task(priority:):(如 .userInitiated, .background 等)
// 用户交互优先级(最高)
let userTask = Task(priority: .userInteractive) {
    await handleUserTap()
}

// 用户发起优先级
let dataTask = Task(priority: .userInitiated) {
    await fetchUserData()
}

// 实用工具优先级
let utilityTask = Task(priority: .utility) {
    await processData()
}

// 后台优先级(最低)
let backgroundTask = Task(priority: .background) {
    await cleanupCache()
}

任务优先级只是建议,不能保证一定被操作系统采纳

🎭 实际应用场景

📱 后台数据加载
class DataManager {
    private var dataTask: Task<[User], Error>?
    
    func loadUsers() async throws -> [User] {
        // 取消之前的任务
        dataTask?.cancel()
        
        // 创建新任务
        dataTask = Task(priority: .userInitiated) {
            let users = try await fetchUsersFromAPI()
            return users
        }
        
        return try await dataTask?.value ?? []
    }
}
⏰ 定时任务与超时控制
func performTaskWithTimeout() async throws -> String {
    let task = Task {
        try await performLongRunningOperation()
        return "操作完成"
    }
    
    let timeoutTask = Task {
        try await Task.sleep(nanoseconds: 5_000_000_000) // 5秒超时
        task.cancel()
        throw TimeoutError()
    }
    
    // 等待任务完成或超时
    do {
        let result = try await task.value
        timeoutTask.cancel() // 操作完成,取消超时任务
        return result
    } catch {
        timeoutTask.cancel() // 超时任务先触发,也取消
        throw error
    }
}
🧮 并发计算与结果收集
func calculateFibonacciNumbers(_ count: Int) async -> [Int] {
    // 批量创建任务
    let tasks = (0..<count).map { index in
        Task(priority: .utility) {
            return fibonacci(index)
        }
    }
    
    var results: [Int] = []
    // 等待任务完成并收集结果
    for task in tasks {
        if let result = try? await task.value {
            results.append(result)
        }
    }
    
    return results.sorted()
}

上面的这种遍历循环的任务可以使用AsyncSequence来实现哦

🚀 Task 的高级特性

  1. Task 的生命周期管理

Task 的生命周期管理是并发编程中的关键概念,它确保任务不会无限期地占用系统资源,避免内存泄漏和资源浪费。

class TaskManager {
    private var activeTasks: Set<Task<Void, Never>> = []
    
    func startTask() {
        let task = Task {
            await performWork()
        }
        activeTasks.insert(task)
        
        // 任务完成后自动清理
        Task {
            await task.value
            activeTasks.remove(task)
        }
    }
    
    func cancelAllTasks() {
        activeTasks.forEach { $0.cancel() }
        activeTasks.removeAll()
    }
}

知识点

✅ 需要手动管理的场景

  • 长期运行的任务管理器
  • 需要动态添加/移除任务的场景
  • 需要精确控制任务生命周期的场景
  • 需要实现自定义任务队列的场景

❌ 不需要手动管理的场景:

  • 结构化任务(使用 TaskGroup 或 async let)
  • 一次性任务执行,局部Task
  • 任务数量固定的并发场景
  1. 任务依赖关系
func processWithDependencies() async {
    // 第一个任务:获取用户信息
    let userTask = Task {
        return try await fetchUserInfo()
    }
    
    // 第二个任务:基于用户信息获取订单
    let orderTask = Task {
        let user = try await userTask.value
        return try await fetchOrders(for: user.id)
    }
    
    // 第三个任务:处理订单数据
    let processTask = Task {
        let orders = try await orderTask.value
        return await processOrders(orders)
    }
    
    // 等待所有任务完成
    let result = await processTask.value
    print("处理完成:(result)")
}

知识点:

  • 隐式依赖链orderTask 依赖 userTaskprocessTask 依赖 orderTask
  • 数据流传递:每个任务的结果作为下一个任务的输入参数
  • 并发启动:所有任务可以同时启动,但执行时会自动等待依赖完成
  • 错误传播:如果 userTask 失败,后续任务会自动取消
  1. 任务取消传播

任务取消传播是结构化并发的核心特性,它确保当父任务被取消时,所有子任务也会被自动取消,避免资源泄漏和无效计算。

在上面的复杂例子中有体现,

知识点

  • 结构化并发:子任务在父任务的作用域内创建,形成层次结构
  • 自动取消传播:父任务取消时,所有子任务自动取消,无需手动管理
  • 资源清理:取消传播确保所有相关资源得到及时释放
  • 协作式取消:子任务需要主动检查 Task.isCancelled 来响应取消

🔍 Task 与其他并发特性的对比

TaskTaskGroupasync let
用途单个异步任务多个相关任务多个独立任务
生命周期手动管理自动管理自动管理
取消控制精确控制组级别控制自动传播
适用场景复杂任务逻辑批量处理简单并发
错误处理灵活统一简单

🚀 TaskGroup - 并发任务组

🔑 关键字与用法

  • withTaskGroup:并发执行一组相关任务
  • group.addTask { ... }:添加子任务
  • for await result in group:收集结果
try await withTaskGroup(of: Int.self) { group in
    for i in 1...5 {
        group.addTask { i * i }
    }
    for await value in group {
        print(value)
    }
}

🎬 典型场景

并发下载、批量数据处理

🛡️ 错误处理与取消

  • ✅ 支持 try/catch,子任务抛错会传播到父任务
  • ✅ 可在组内取消所有任务(group.cancelAll()

⚠️ 注意事项

  • 子任务间相互独立,不能直接通信
  • 适合任务数量较多、耗时较长的场景
  • 不要把 TaskGroup 的实例泄漏到外部,类似下面的写法。因为withTaskGroup 返回前会先等待所有的子 Task 执行完毕,然后将 TaskGroup 销毁。
guard let group = taskGroup else {
    print("group is nil")
    return
}
  • 也不要在子任务中修改taskgroup,子Task 的执行体可能会被调度到不同的线程上,这样就导致对 TaskGroup 的修改是并发的,不安全
await withTaskGroup(of: Void.self) { (group) -> Void in
    group.addTask {
        group.addTask { // error!
            print("inner task")
        }
    }
}

⚡ async let - 并发声明

🔑 关键字与用法

  • async let:声明并发异步变量,自动并发执行,await 等待结果
async let a = fetchA()
async let b = fetchB()
let result = await (a, b)

🎯 适用场景

  • 🔄 多个独立异步操作并发执行
  • ⚖️ 比 TaskGroup 更轻量,适合少量并发

⚠️ 注意事项

  • async let 变量作用域为当前函数
  • 不能跨函数传递

🎭 Actor 模型基础 - 数据隔离的核心

Actor 是 Swift 并发编程中实现数据隔离的核心机制,它通过串行队列确保同一时间只有一个任务能访问 Actor 的内部状态,从而避免数据竞争

🔑 关键字与用法

  • actor:声明Actor类型
  • Actor内部状态自动隔离,线程安全
  • 通过 await 访问Actor方法
actor Counter {
    private var value = 0
    func increment() { value += 1 }
    func getValue() -> Int { value }
}
let counter = Counter()
await counter.increment()

🎯 实际应用场景

共享状态管理、计数器、缓存等

🚀 Actor 的工作原理

  1. 内部机制
// Actor 的内部实现原理(简化版)
actor BankAccount {
    private var balance: Double = 1000.0
    private var transactions: [Transaction] = []
    
    func deposit(_ amount: Double) {
        balance += amount
        transactions.append(Transaction(type: .deposit, amount: amount))
    }
    
    func withdraw(_ amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        transactions.append(Transaction(type: .withdraw, amount: amount))
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

知识点

  • 串行队列:每个 Actor 都有一个内部的串行队列
  • 状态隔离balancetransactions 只能在 Actor 内部直接访问
  • 方法调用:所有外部调用都会进入队列,按顺序执行
  • 数据一致性:确保 balancetransactions 始终保持同步
  1. 并发访问示例
func testConcurrentAccess() async {
    let account = BankAccount()
    
    // 创建多个并发任务
    await withTaskGroup(of: Void.self) { group in
        // 任务1:存款
        group.addTask {
            await account.deposit(100)
            print("存款100完成")
        }
        
        // 任务2:取款
        group.addTask {
            let success = await account.withdraw(50)
            print("取款50: (success ? "成功" : "失败")")
        }
        
        // 任务3:查询余额
        group.addTask {
            let balance = await account.getBalance()
            print("当前余额: (balance)")
        }
    }
}

技术要点

  • 并发安全:多个任务可以同时调用 Actor 方法
  • 顺序执行:虽然调用是并发的,但执行是串行的
  • 结果一致性:无论执行顺序如何,最终状态都是一致的
  • 性能影响:串行执行可能影响性能,但保证了数据安全
  1. 状态隔离与访问控制
actor UserProfile {
    private var profile: Profile
    private var lastUpdated: Date
    
    // 只读属性:外部可以同步访问
    nonisolated var displayName: String {
        return profile.displayName
    }
    
    // 需要隔离的方法
    func updateProfile(_ newProfile: Profile) {
        profile = newProfile
        lastUpdated = Date()
    }
    
    // 内部方法:不需要 await
    private func validateProfile(_ profile: Profile) -> Bool {
        return !profile.displayName.isEmpty
    }
}

技术要点

  • nonisolated:标记不需要隔离的方法/属性,可以同步访问
  • 私有方法:Actor 内部的方法调用不需要 await
  • 状态一致性:所有状态修改都通过隔离方法进行
  • 性能优化:只读属性可以避免不必要的异步调用

⚠️ 注意事项

  • Actor方法默认是异步的
  • 不能直接跨Actor同步访问
  • Actor之间通信需通过异步消息

🎨 MainActor 基础

🔑 关键字与用法

  • @MainActor:标记类/方法在主线程执行

用于UI更新、主线程安全

@MainActor
class ViewModel: ObservableObject {
    @Published var text: String = ""
    func updateText(_ newText: String) {
        text = newText
    }
}

🎯 典型场景

SwiftUI、UIKit界面更新

⚠️ 注意事项

  • @MainActor 保证所有标记方法/属性在主线程访问
  • 避免在主线程执行耗时操作

🚀 高级知识 - 专家级技能

🎭 Actor 高级特性

  1. 继承与限制

Actor可继承(Swift 5.9+),但有并发限制

actor BaseActor {
    var counter = 0
    
    func sayHello() {
        print("Hello from base, counter: (counter)")
    }
}

actor ChildActor: BaseActor {
    override func sayHello() {
        print("Hello from child")
    }
}

不能直接同步调用其他Actor方法

func test() async {
    let child = ChildActor()

    // ❌ 直接访问存储属性会报错(跨 actor 隔离)
    // print(child.counter)

    // ✅ 必须通过 await 调用 actor 的方法
    await child.sayHello()
    await child.sayHello()
}

Swift 的 actor 本质上是引用类型 + 隔离队列。

所谓并发限制:即使 ChildActor 继承了 BaseActor,但它们:

  • 共享父类里的 actor 隔离规则
  • 不能跨 actor 隔离直接访问父类存储属性
  • 调用方法时依然需要遵守 await 规则
  1. Actor 之间的通信

actor OrderProcessor {
    private var orders: [Order] = []
    
    func addOrder(_ order: Order) {
        orders.append(order)
    }
    
    func processOrders() async {
        for order in orders {
            // 调用其他 Actor 的方法
            await inventoryManager.reserveItems(for: order)
            await paymentProcessor.processPayment(for: order)
        }
    }
}

actor InventoryManager {
    private var inventory: [String: Int] = [:]
    
    func reserveItems(for order: Order) async -> Bool {
        // 库存检查逻辑
        return true
    }
}

actor PaymentProcessor {
    private var inventory: [String: Int] = [:]
    
    func processPayment(for order: Order) async -> Bool {
        // 库存检查逻辑
        return true
    }
}

技术要点

  • 跨 Actor 调用:必须使用 await,因为可能涉及队列等待
  • 消息传递:Actor 之间通过异步方法调用进行通信
  • 状态隔离:每个 Actor 维护自己的状态,不会相互干扰
  • 并发安全:多个 Actor 可以同时处理不同的请求
  1. 数据竞争问题

多线程同时读写同一变量,导致结果不可预测

class Counter {
    private var count = 0
    func increment() { count += 1 }
    func getCount() -> Int { count }
}

func testDataRace() async {
    let counter = Counter()
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<1000 {
            group.addTask { counter.increment() }
        }
    }
    print("最终计数:(counter.getCount())") // 可能不是1000
}

✅ 解决方案

使用Actor(推荐)

使用锁(NSLock、DispatchQueue)

使用原子操作(OSAtomic)

actor SafeCounter {
    private var count = 0
    func increment() { count += 1 }
    func getCount() -> Int { count }
}

🔍 Actor vs 其他并发安全方案

ActorNSLockDispatchQueue@MainActor
线程安全✅ 自动✅ 手动✅ 手动✅ 主线程
性能🟡 中等🟢 高🟢 高🟡 中等
易用性🟢 简单🟡 中等🟡 中等🟢 简单
灵活性🟡 中等🟢 高🟢 高🟡 中等
内存安全🟢 自动🟡 手动🟡 手动🟢 自动

🚫 死锁预防与内存管理

🚨 死锁场景

嵌套Actor调用、资源互锁

actor ResourceA {
    var resourceB: ResourceB?

    func accessB() async {
        guard let b = resourceB else { return }
        await b.doSomething()
    }
}

actor ResourceB {
    var resourceA: ResourceA?

    func doSomething() async {
        guard let a = resourceA else { return }
        await a.accessB()  // ❌ 死锁风险
    }
}

Swift 的每个 actor 都有自己的串行执行队列,任何访问它隔离域的代码,都必须进入该队列

🛡️ 预防方法

  1. 使用弱引用缓存、及时释放资源
weak var resourceB: ResourceB?

2. 使用 nonisolated 方法,如果某些方法不需要访问 actor 隔离的数据,可以声明为:

nonisolated func doSomethingPublic() {
    // ...
}

⚡ 性能优化技巧

📊 Task数量与批量处理

避免创建过多Task,采用批量分组

func efficientProcessing() async {
    let items = Array(1...10000)
    let batchSize = 100
    await withTaskGroup(of: Void.self) { group in
        for i in stride(from: 0, to: items.count, by: batchSize) {
            let end = min(i + batchSize, items.count)
            let batch = Array(items[i..<end])
            group.addTask {
                for item in batch {
                    processItem(item)
                }
            }
        }
    }
}

上面的代码使用结构化并发的 withTaskGroup,自动等待所有子任务完成。把 items 分成每 100 个为一组的 batch。每组数据都通过 group.addTask { ... } 放入一个并发子任务中执行。每个子任务中顺序执行本组的 100 个 item。这样处理之后,1万个任务切成100个批次,只创建了100个并发任务。

🎯 优先级管理

合理设置Task优先级,保障用户体验

func priorityManagement() async {
    let userTask = Task(priority: .userInteractive) { await handleUserTap() }
    let dataTask = Task(priority: .userInitiated) { await processUserData() }
    let cleanupTask = Task(priority: .background) { await cleanupCache() }
    await userTask.value
    await dataTask.value
    await cleanupTask.value
}

✅ 内存优化

使用弱引用、缓存清理

actor OptimizedCache {
    private var cache: [String: WeakReference<Data>] = [:]
    func store(_ data: Data, for key: String) { cache[key] = WeakReference(data) }
    func retrieve(for key: String) -> Data? { cache[key]?.value }
    func cleanup() { cache = cache.filter { $0.value.value != nil } }
}
class WeakReference<T: AnyObject> {
    weak var value: T?
    init(_ value: T) { self.value = value }
}

上面的代码实现了一个自动清理失效对象的“弱引用缓存”容器。用于防止缓存强引用对象,避免内存泄露

并发线程模型总结:

Concurrency 的本质

Swift Concurrency 并发在底层使用的是一种新实现的协同式线程池 (cooperative thread pool) 的调度方式: 由一个串行队列负责调度工作,它将函数中剩余的运行内容被抽象为更轻量的续体 (continuation),来进行调度

传统的 GCD 模式的局限性

在 Concurrency 库出现之前,Swift 是采用的 GCD 方式,将不同 thread 运行在不同内核上,可能出现多个 thread 在一个内核上运行的 case,单个线程的内存占用达到 500KB - 1MB。因此线程创建本身也是一件带有内存消耗的事情,在并发 queue 中如果任务未完成执行,那么 queue 会倾向于重新创建 thread,直到达到并发队列的上限(64) 为止。

🎯 实战应用 - 真实场景

📤 场景一:意见反馈日志多文件上传

需要同时上传多个文件,每个文件上传可能成功或失败,需要记录详细日志。

actor File {
    let fileName: String
    private var uploadError: Error? = nil
    
    init(fileName: String) {
        self.fileName = fileName
    }

    func setUploadError(_ error: Error) {
        self.uploadError = error
    }

    func hasUploadError() -> Bool {
        return uploadError != nil
    }
}

class Logger: @unchecked Sendable {
    func log(level: String, msg: String) {
        print("[(level)] (msg)")
    }
}

actor Uploader {
    let logger = Logger()
    
    func upload(multiFile: File) async {
        let fileName = multiFile.fileName
        logger.log(level: "info", msg: "开始上传多文件: (fileName)")
        
        let randomSeconds = Int.random(in: 2...5)
        let delay = UInt64(randomSeconds) * 1_000_000_000 // 转换为纳秒
        try? await Task.sleep(nanoseconds: delay)

        if Bool.random() {
            await multiFile.setUploadError(NSError(domain: "UploadError", code: 1, userInfo: nil))
        }
    }

    func startUploading(files: [File]) async {
        await withTaskGroup(of: Void.self) { group in
            for file in files {
                group.addTask {
                    await self.upload(multiFile: file)
                    
                    let fileName = file.fileName
                    let hasError = await file.hasUploadError()
                    
                    if hasError {
                        self.logger.log(level: "error", msg: "文件(fileName)上传失败,上传流程中断")
                    } else {
                        self.logger.log(level: "info", msg: "文件(fileName)上传流程完成")
                    }
                }
            }
            self.logger.log(level: "info", msg: "等待所有上传任务执行完成")
            await group.waitForAll()
            self.logger.log(level: "info", msg: "所有上传任务执行完成")
        }
        self.logger.log(level: "info", msg: "所有文件上传流程完成")
    }
}

let uploader = Uploader()

let files = [
    File(fileName: "file1.txt"),
    File(fileName: "file2.txt"),
    File(fileName: "file3.txt"),
    File(fileName: "file4.txt"),
    File(fileName: "file5.txt"),
    File(fileName: "file6.txt"),
    File(fileName: "file7.txt"),
    File(fileName: "file8.txt")
]

Task {
    await uploader.startUploading(files: files)
    print("所有日志上传文件结束")
}

对比一下以前的写法

💡 关键知识点

@unchecked Sendable
  • @unchecked Sendable是一个属性包装器,表示某个类或结构体是 Sendable,允许在并发任务中安全地跨线程传递。Sendable 确保对象可以在多个线程间传递而不发生数据竞争。
  • Logger 类被标记为 @unchecked Sendable,表示它可以在多个并发任务中传递。在 Swift 并发模型中,如果某个对象被标记为 @unchecked Sendable,意味着该对象的 线程安全性 没有被检查,所以需要我们非常注意,这个被标记的类是否真的是线程安全,这里Logger只是一个打印类,所以可以被标记
withTaskGroup

Task Group 用于将多个异步任务组合成一个并发任务组,并且可以等待所有任务完成。任务组让多个异步任务并发执行,且可以等待所有任务完成。

UploaderstartUploading 方法中,使用了 withTaskGroup 来并行上传多个文件,并在所有任务完成后输出最终日志。

🔄 场景二:串行异步任务队列

有时候经常需要在不同地方将数据插入数据库,通过TaskQueue来管理加入的任务,让他们按顺序插入,防止数据竞争

/// 利用actor达到防止插入数据库的数据资源竞争问题
actor TaskQueue {
    private var lastTask: Task<Void, Never>?

    func addTask(_ task: @escaping () async -> Void) {
        let previousTask = lastTask // 复制 lastTask,防止 actor 内部 await 自己
        let newTask = Task {
            // 在 actor 外部等待上一个任务完成,避免死锁
            await previousTask?.value
            await task()
        }
        lastTask = newTask
    }
}

// 全局队列
let taskQueue = TaskQueue()
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // 在多个地方调用
        Task { await insertData("数据 1") }

    }
    
    // 模拟异步插入数据库函数
    func insertData(_ value: String) async {
        await taskQueue.addTask {
            print("开始插入数据: (value) at (Date())")
            try? await Task.sleep(nanoseconds: 2_000_000_000) // 模拟数据库插入
            print("完成插入数据: (value) at (Date())")
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Task { await insertData("数据 2") }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        Task { await insertData("数据 3") }
    }
}

💡 关键知识点

actor
  • 线程隔离actor 会自动确保它的内部状态只能被 一个任务 同时访问和修改。通过这种方式,actor 可以保证在并发环境下不会发生 数据竞争资源冲突
  • 异步执行actor 的方法通常是 异步的,并且 需要通过 await 关键字来访问或修改其状态,这意味着它们会在异步上下文中执行,防止阻塞其他操作。

在上面的代码中,在 addTask 方法中,我们复制了 lastTask,并创建了一个新的 Task。每当有新任务加入时,它会等待读取 上一个任务 的结果(通过 await previousTask?.value),这就实现了 任务的串行执行。新任务只有在前一个任务完成后才会开始执行。

防止死锁:在 TaskQueue 内部,我们通过复制lastTask 来避免 actor 内部直接 await 自己的任务,这样可以防止在任务链中发生死锁。如果直接调用 await lastTask?.value,可能会导致任务的循环等待,从而产生死锁。

🧵 场景三:Task 内部线程问题

现在,只要是系统的UI组件,都默认加了@MainActor标志,同时 @Published , @State , @Binding 等属性包装器在swift6的强制模式下,都要求必须在主线程调用,否则会报错

这也就意味着,只要是外部调用了带有@MainActor标志的类的函数或者属性,默认都是在主线程执行的。

class ViewController: UIViewController {

    var viewModel = ViewModel()
    var label: UILabel!
    var cancellables = Set<AnyCancellable>()
    override func viewDidLoad() {
        super.viewDidLoad()
        // 设置 UILabel
        label = UILabel()
        label.frame = CGRect(x: 20, y: 100, width: 300, height: 40)
        label.text = "Loading..."
        view.addSubview(label)
        
        test1()
    }
}

extension ViewController {
    func test1() {
        viewModel.$data
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { text in
                self.label.text = text
            })
            .store(in: &cancellables)
        Task {
            print("开始查询: (Thread.current)")
            await viewModel.fetchData() // 异步调用 ViewModel 的网络请求
            await viewModel.fetchData1() // 异步调用 ViewModel 的网络请求
//            label.text = viewModel.data  // ❌
            print("查询结束: (Thread.current)")
        }
    }
}

class ViewModel: ObservableObject {
    @Published var data: String = "Loading..."

    func fetchData() async {
        Task {
            print("Start fetching data on thread: (Thread.current)")
            try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)  // 模拟耗时操作 1 秒
            print("Data fetched on thread: (Thread.current)")

//            await MainActor.run {
                self.data = "Failed to load data"
//            }
        }
    }
    
    func fetchData1() async {
        Task.detached {
            print("Start fetching data on thread: (Thread.current)")
            try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)  // 模拟耗时操作 1 秒
            print("Data fetched on thread: (Thread.current)")

            await MainActor.run {
                self.data = "Failed to load data"
            }
        }
    }
}
  1. 尝试去掉fetchData()函数的 Task 看看效果
  2. 尝试给test1包一个DispatchQueue.global().async 看看效果

💡 关键知识点

TaskTask.detached
  • Task 它允许我们异步地执行某个操作,并自动将其调度到适当的执行上下文中(默认情况下是在调用它的线程上下文中执行,通常是主线程)。
  • Task.detached:它允许在完全独立的上下文中执行任务,不受调用上下文的影响,通常用于后台操作。
Context

关于上下文的理解,官方文档还是一如既往的描述少的可怜。

Swift 并发中所谓的「上下文」并不等于线程上下文(即你在哪个线程调用) ,而是任务调度上下文(包括 actor、任务父子关系等)

  1. 结构化任务上下文

    1. 当前是不是在某个 Task 的作用域内,它会继承父任务的调度策略
  2. actor 上下文

    1. 如果在某个 actor 中调用,它会绑定到该 actor 的隔离上下文
  3. MainActor

    1. 调度在主线程
  4. 当前调用线程

    1. 仅作为一个调度 hint,不能完全决定 Task 执行线程

这里还有一些迷惑的点,如果没有父Task,那么他的调度策略是啥样的???不知道,完全由系统决定,可能是主线程,也可能是子线程

可以确定的是Task.detached创建的分离任务执行的线程基本是非主线程。

🚫 场景四:在 Task 内部使用信号量导致死锁

有不少Task下执行的函数内部还有Dispatch的信号量。

当这个函数只执行一次的时候是没有问题的,这里我将逻辑抽离成DailyInsightRepository

DispatchQueue.global().async {
    DailyInsightRepository().testDeadlockWithTask()
}

class DailyInsightRepository: NSObject {
    // 模拟一个需要等待的网络请求函数
        func fetchDailySleepUpdateIndentity(on dayString: String, completion: @escaping (String?) -> Void) {
            // 模拟一个网络请求的延迟
            DispatchQueue.global().async {
                print("Fetching sleep data for (dayString) on (Thread.current)")
                // 模拟延迟1秒
                sleep(10)
                // 返回一个睡眠标识符
                completion("sleepIdentifierFor(dayString)")
            }
        }

        // 需要通过信号量来等待网络请求的函数
        func needUpdateCache(dayString: String) -> String {
            let semaphore = DispatchSemaphore(value: 0)
            var nowIdentifier: String?

            // 在后台执行模拟的网络请求
            fetchDailySleepUpdateIndentity(on: dayString) { identifier in
                nowIdentifier = identifier
                semaphore.signal()  // 一旦网络请求完成,释放信号量
            }

            // 阻塞当前线程,直到信号量被释放
            semaphore.wait()

            // 如果没有获取到有效的标识符,返回 "noData"
            guard let identifier = nowIdentifier else {
                return "noData"
            }

            return "Fetched data with identifier: (identifier)"
        }

        // 使用 Task 来调用 needUpdateCache
        func testDeadlockWithTask() {
            Task {
                print("Task started on (Thread.current)")

                // 调用 needUpdateCache,模拟异步任务中的死锁问题
                let result = needUpdateCache(dayString: "2023-08-01")
                
                // 打印结果
                print("Result: (result)")

                print("Task finished on (Thread.current)")
            }
        }
}

但是当并发执行的时候,这里的Task因为继承了上下文,可能就会在同一个线程中产出互相等待,从而造成死锁

DispatchQueue.global().async {
    self.repo.testDeadlockWithTask()
    self.repo.testDeadlockWithTask()
}

class DailyInsightRepository: NSObject {
// 模拟一个需要等待的网络请求函数
func fetchDailySleepUpdateIndentity(on dayString: String, completion: @escaping (String?) -> Void) {
    // 模拟一个网络请求的延迟
    DispatchQueue.global().async {
        print("Fetching sleep data for (dayString) on (Thread.current)")
        // 模拟延迟1秒
        sleep(10)
        // 返回一个睡眠标识符
        completion("sleepIdentifierFor(dayString)")
    }
}

// 需要通过信号量来等待网络请求的函数
func needUpdateCache(dayString: String) -> String {
    print("needUpdateCache (Thread.current)")
    let semaphore = DispatchSemaphore(value: 0)
    var nowIdentifier: String?

    // 在后台执行模拟的网络请求
    fetchDailySleepUpdateIndentity(on: dayString) { identifier in
        nowIdentifier = identifier
        semaphore.signal()  // 一旦网络请求完成,释放信号量
    }

    // 阻塞当前线程,直到信号量被释放
    semaphore.wait()

    // 如果没有获取到有效的标识符,返回 "noData"
    guard let identifier = nowIdentifier else {
        return "noData"
    }

    return "Fetched data with identifier: (identifier)"
}

// 使用 Task 来调用 needUpdateCache
func testDeadlockWithTask() {
    Task {
        print("Task started on (Thread.current)")

        // 调用 needUpdateCache,模拟异步任务中的死锁问题
        let result = needUpdateCache(dayString: "2023-08-01")
        
        // 打印结果
        print("Result: (result)")

        print("Task finished on (Thread.current)")
    }
}
}

✅ 解决方案:使用 async/await

第一种,就是使用 testDeadlockWithTask 每次调用都是用一个Task包裹,

Task {
    self.repo.testDeadlockWithTask()
}

Task {
    self.repo.testDeadlockWithTask()
}

但是这样的调用,在其内部实际上是串行执行,并非并发执行

第二种方案:使用 async/await 替代信号量

class DailyInsightRepository1: NSObject {
    // 模拟一个需要等待的网络请求函数
    func fetchDailySleepUpdateIndentity(on dayString: String) async -> String? {
        // 模拟一个网络请求的延迟
        print("Fetching sleep data for (dayString) on (Thread.current)")
        // 模拟延迟1秒
        try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
        // 返回一个睡眠标识符
        return "sleepIdentifierFor(dayString)"
    }

    // 使用 async/await 来改进 needUpdateCache 函数
    func needUpdateCache(dayString: String) async -> String {
        print("needUpdateCache (Thread.current)")

        // 异步获取睡眠标识符
        if let nowIdentifier = await fetchDailySleepUpdateIndentity(on: dayString) {
            return "Fetched data with identifier: (nowIdentifier)"
        }

        return "noData"
    }

    // 使用 Task 来调用 needUpdateCache
    func testDeadlockWithTask() {
        Task {
            print("Task started on (Thread.current)")

            // 调用 needUpdateCache,模拟异步任务中的死锁问题
            let result = await needUpdateCache(dayString: "2023-08-01")
            
            // 打印结果
            print("Result: (result)")

            print("Task finished on (Thread.current)")
        }
    }
}

优先推荐第二种方案,因为第二种方案在支持并发执行的情况下也不会有线程阻塞

💡 关键知识点

这个场景的知识点,其实和上面的几个场景类似,主要是Task的线程以及配合async和await相关的操作

💡 最佳实践与常见陷阱

🔑 并发中的关键字总结

在并发过程中常见的关键字如下:

async/await TaskactorTaskGroup@MainActor@Sendable @nonisolated

其中大多数在上面的课程中已经说过。接下来简单介绍未触及到的关键字

nonisolated

提供非隔离访问,表示它不依赖于 actor 的状态, 即使在 actor 外部 也可以直接同步调用(无需 await)。


actor Logger {
    // 非隔离属性(常量),可以同步访问
    nonisolated let version = "1.0.0"

    // 非隔离方法:不访问任何 actor 状态,可以在 actor 外部直接调用
    nonisolated func staticInfo() -> String {
        "Logger version (version), created by ChatGPT"
    }

    // actor 内部状态
    private var logs: [String] = []

    // 隔离方法,必须 await 才能访问
    func log(_ message: String) {
        logs.append("[(Date())] (message)")
    }

    func allLogs() -> [String] {
        logs
    }
}

// 🌟 调用者
func testLogger() {
    let logger = Logger()

    // ✅ nonisolated 方法/属性:可以同步调用,无需 await
    print(logger.version)
    print(logger.staticInfo())

    // ❗️下面是 actor 隔离方法,必须 await 调用
    Task {
        await logger.log("App started")
        await logger.log("User tapped button")

        let logs = await logger.allLogs()
        print("📃 日志内容:\n(logs.joined(separator: "\n"))")
    }
}

@TaskLocal

@TaskLocal 是一个新引入的关键字,用于在任务之间共享数据。它允许你将某些状态与当前的任务绑定,而不是将其与全局状态绑定。@TaskLocal 可以确保每个任务都有自己的独立数据副本,避免任务之间的干扰。

import Combine

// 定义一个 TaskLocal 变量
enum TraceID {
    @TaskLocal static var current: String?
}

func log(_ message: String) {
    print("[(TraceID.current ?? "no-trace")] (message)")
}

func doWork() async {
    log("任务开始")
    await try? Task.sleep(for: .seconds(1))
    log("任务结束")
}

struct MyApp {
    static func main() async {
        // 设置 TaskLocal 的值
        await TraceID.$current.withValue("TRACE-12345") {
            await doWork()
        }
        
        // 这里 TraceID.current 已经恢复默认
        await doWork()
    }
}

Task {
    await MyApp.main()
}

@TaskLocal 修饰的属性可以确保每个任务在执行时都有自己的局部数据副本,避免共享全局状态。

🚨 常见陷阱与解决方案

  1. 死锁问题

  • 原因:嵌套 Actor 调用、资源互锁
  • 解决:使用弱引用、nonisolated 方法
  1. 数据竞争

  • 原因:多线程同时读写同一变量
  • 解决:使用 Actor、锁、原子操作
  1. 线程阻塞

  • 原因:在 Task 中使用信号量
  • 解决:使用 async/await 替代
  1. 内存泄漏

  • 原因:强引用循环
  • 解决:使用弱引用、及时清理

🎯 性能优化建议

  1. 合理使用 Task 数量:避免创建过多 Task,采用批量分组
  2. 优先级管理:合理设置 Task 优先级,保障用户体验
  3. 内存优化:使用弱引用、缓存清理
  4. 错误处理:及时处理异常,避免资源浪费

🚀 学习资源推荐

📚 官方文档

Swift Concurrency

docs.swift.org/swift-book/…

🎥 视频教程

Meet async/await in Swift - WWDC21 - Videos - Apple Developer

Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer