并发编程

开篇故事

想象你在一家餐厅的后厨工作。厨师长(主线程)负责摆盘和最终检查。但切菜、炒菜、洗碗这些活不能全让厨师长干。你需要帮手。

在 Swift 过去,我们用 Thread(线程)和 Semaphore(信号量)"人肉管理"这些帮手。就像厨师长站在厨房门口大喊"A去切菜,B去炒菜"。但这有个问题。如果两人都同时去拿同一把刀,就会打架。这叫做"数据竞争"。

Swift 的并发编程是现代化的厨房管理系统。你只需要说"帮我准备这道菜",系统自动分配人手,还能保证不会有人拿到同一把刀。

Sources/BasicSample/ConcurrencySample.swift 中有完整的并发示例。让我们从最基础的 async/await 开始。


本章适合谁

如果你写过网络请求、文件读写,或者任何需要等待外部资源的操作,你就能从并发编程中受益。本章适合有一定 Swift 基础的学习者。如果你是第一次接触 Swift,建议先完成 错误处理闭包


你会学到什么

完成本章后,你可以:

  1. 使用 async/await 语法编写异步代码
  2. 理解 async let 实现并行绑定
  3. 使用 Task 创建和管理后台任务
  4. TaskGroup 实现动态并行计算
  5. 掌握 actorSendable 实现线程安全

前置要求

你需要先理解 Swift 的闭包 (closure)、错误处理 (error handling) 和可选类型 (optional)。如果还没学过,请先阅读 错误处理闭包


第一个例子

打开 Sources/BasicSample/ConcurrencySample.swift,我们从最简单的异步函数开始:

func fetchUser(id: Int) async throws -> String {
    try await Task.sleep(for: .seconds(1))  // 模拟网络延迟
    return "User \(id)"
}

// 调用异步函数需要 await
Task {
    let user = try await fetchUser(id: 1)
    print(user)  // User 1
}

发生了什么?

  • async 标记函数是异步的,意味着它可以在执行过程中暂停
  • await 表示"暂停当前代码,等这个异步操作完成再继续"
  • throws 表示函数可能抛出错误

关键区别:和传统的回调方式不同,代码写起来就像同步代码一样await 暂停后,继续往下执行。没有回调地狱 (callback hell)。


原理解析

1. async/await 基本概念

异步函数的调用看起来就是普通的函数调用:

@available(macOS 13.0, *)
func generateSlideshow(forGallery gallery: String) async throws {
    let photos = try await listPhotos(inGallery: gallery)  // 等待获取照片

    for photo in photos {
        await Task.yield()  // 让出线程,允许其他任务运行
        print("photo:", photo)
    }
}

关键点

  • async 函数只能在另一个 async 函数或 Task 中被调用
  • await 是标记,告诉编译器"这里有暂停的可能"
  • 代码从上到下阅读,就像普通的同步代码

2. async let — 并行绑定

当你需要同时启动多个独立任务时,async let 让绑定是异步的,但后续再 await

async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)

do {
    let users = try await [user1, user2, user3]
    print("Fetched users: \(users)")
    // 三个请求是同时发出的,不是等第一个完第二个才开始
} catch {
    print("Error: \(error)")
}

这三个请求是并行的。如果每个请求需要 1 秒,三个并行总共还是 1 秒左右(忽略网络开销)。

如果每个请求顺序执行则需要 3 秒。

async let 的妙处在于:声明后任务立即开始,你可以先做其他事,等真的需要结果时再 await

3. Task — 创建并发任务

Task 是并发编程的基本单元:

// 创建一个新任务
let task = Task {
    print("Running in background")
    let result = try await fetchUser(id: 42)
    return result
}

// 等待任务完成并获取结果
let user = try await task.value

Task.detached 创建的是不继承当前上下文的独立任务:

let detached = Task.detached {
    // 这个任务不继承当前任务的优先级、本地存储等
    print("Independent task")
}

类比

Task 就像你给厨师长派个帮手,帮手继承了厨师长的工作环境。Task.detached 则是另招一个新人,从头开始。

4. TaskGroup — 动态并行

当你不知道需要创建多少任务时(比如处理文件列表中的每个文件),用 TaskGroup:

// 来自 ConcurrencySample.swift 的 BatchCounter 示例
await withTaskGroup(of: Void.self) { group in
    for i in 1...1_000_000 {
        group.addTask {
            await counter.increment()
        }
    }
    // 退出闭包时,自动等待所有任务完成
}
  • withTaskGroup 创建一个任务组
  • addTask 向组中添加任务
  • 闭包结束时自动 await 所有任务完成

这比手动管理 100 万个 Task 方便得多。Swift 底层会帮你调度,充分利用多核 CPU。

5. @MainActor — 主线程隔离(MainActor)

在 iOS/macOS 应用中,更新 UI 必须在主线程上:

@MainActor
func updateUI() {
    // 这个方法保证在主线程执行
    label.text = "Updated!"
    imageView.image = newImage
}

// 在后台任务中调用:
Task {
    let data = try await fetchFromNetwork()
    // 回到主线程更新 UI
    await MainActor.run {
        updateUI()
    }
}

@MainActor 是 Swift 对主线程的标注。被它标注的函数/类只能在主线程调用。编译器会确保这一点,不需要你手动检查。

6. Sendable — Swift 6.0 严格并发

Swift 6.0 引入了 Strict Concurrency(严格并发),核心是 Sendable 协议:

// 不可变数据天然安全
struct Config: Sendable {
    let apiKey: String
    let timeout: Int
}

// 引用类型需要显式标注
actor UserCache: Sendable {
    private var cache: [String: String] = [:]

    func get(_ key: String) -> String? {
        cache[key]
    }
}

Sendable 意味着"这个类型的值可以安全地跨线程传递"。Swift 6.0 编译器会在编译时检查所有跨线程数据传递,从根本上杜绝数据竞争。

传统方式 vs Swift 6.0:

  • 传统方式:运行时崩溃(数据竞争),需要你用锁、信号量等手动避免
  • Swift 6.0:编译时检查,不符合 Sendable 的代码无法通过编译,把数据竞争消灭在编译阶段

7. async/await vs 传统回调 Completion Handler 的写法:

// 传统回调方式
func fetchUser(id: Int, completion: (Result<String, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        // ...
    }
}
// async/await 方式
func fetchUser(id: Int) async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8)!
}

对比之下,async/await 方式

  • 代码更简洁,没有嵌套的回调
  • 错误处理用 try/catch,更直观
  • 调试友好,断点可以正常设置
  • 避免了回调地狱 (callback hell)

8. AsyncSequence — 异步序列

对于需要持续接收的数据(比如传感器读数、实时消息),用 AsyncSequence

// 来自 ConcurrencySample.swift 的 SensorManager
class SensorManager {
    func startMonitoring() -> AsyncStream<String> {
        let (stream, continuation) = AsyncStream.makeStream(of: String.self)

        Task {
            let dataPoints = ["25°C", "26°C", "27°C"]
            for point in dataPoints {
                try await Task.sleep(for: .seconds(1))
                continuation.yield(point)  // 发送数据
            }
            continuation.finish()  // 结束流
        }

        return stream
    }
}

// 消费端 — for-await 循环
for await value in sensorStream {
    print("Received: \(value)")
}

AsyncSequence 和普通的 Sequence 类似,只是迭代时要用 for await。这非常适合需要流式处理数据的场景。

9. withCheckedContinuation — 桥接旧 API

当你要用旧有的 Completion Handler 包装成 async 函数:

func loadResource(url: URL) async throws -> Data {
    // 旧 API:URLSession.dataTask 使用回调
    try await withCheckedThrowingContinuation { continuation in
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = data {
                continuation.resume(returning: data)
            }
        }
        task.resume()
    }
}

withCheckedContinuation 把你的回调"桥接"成 async 函数。编译器会检查是否 resume 了,如果忘记调用 resume 会崩溃(开发阶段提醒)。


常见错误

错误 1: 忘记 await

func fetch() async -> String { "data" }

let data = fetch() // ❌ 编译错误!

编译器输出:

error: call to async initializer 'fetch()' in a synchronous function

修复方法:

let data = await fetch() // ✅ 加上 await

错误 2: 在非 async 函数中调用 async 函数

func syncFunction() {
    let data = await fetch() // ❌ 不能在同步函数中使用 await
}

编译器输出:

error: 'async' call in a function that does not support concurrency

修复方法: 把外层函数也标记为 async,或者用 Task 包装:

func syncFunction() {
    Task {
        let data = await fetch()
    }
}

错误 3: 数据竞争(违反 Sendable)

class Counter {
    var count = 0

    func increment() {
        count += 1  // 多线程调用会出问题
    }
}

// Swift 6.0 编译器报错
Task.detached {
    await counter.increment() // 可能报错
}

编译器输出:

error: reference to class 'Counter' is not concurrency-safe

修复方法: 使用 actor 替代 class:

actor Counter {
    var count = 0

    func increment() {
        count += 1  // ✅ 安全,actor 保证串行访问
    }
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
异步函数async defasync fnasync 函数语法几乎一样
等待操作awaitawaitawait完全一致
协程/任务asyncio.Tasktokio::TaskTask都类似
并行绑不支持直接 join TaskGroup`async letPython 需手动
共享数据GIL(锁)Arc<Mutex<T>>Actor + SendableSwift 编译时检查
数据竞争保护所有权系统 + SendableSendable + Strict ConcurrencySwift 6.0 编译时
异步流async forStream traitAsyncSequence都有

动手练习

练习 1: async let 并行加载

写一个函数,同时发起 3 个模拟网络请求(每个延迟 1 秒),总共只需约 1 秒。用 async let 实现。

点击查看答案
func mockFetch(id: Int) async -> String {
    await Task.sleep(for: .seconds(1))
    return "Result \(id)"
}

func runParallel() async {
    async let r1 = mockFetch(id: 1)
    async let r2 = mockFetch(id: 2)
    async let r3 = mockFetch(id: 3)

    let results = await [r1, r2, r3]
    print("All done: \(results)")
}
// 总耗时约 1 秒(并行),而非 3 秒(串行)

练习 2: 用 actor 实现线程安全计数器

写一个 actor 实现计数器,包含 increment()getCount() 方法。创建 10 个并发任务各调用 increment 100 次,最后打印结果。

点击查看答案
actor ThreadSafeCounter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getCount() -> Int {
        value
    }
}

func testCounter() async {
    let counter = ThreadSafeCounter()

    // 10个并发任务,每个加100次
    let tasks = (1...10).map { _ in
        Task {
            for _ in 0..<1000 {
                await counter.increment()
            }
        }
    }

    await withTaskGroup(of: Void.self) { group in
        for task in tasks {
            group.addTask {
                await task.value
            }
        }
    }

    let final = await counter.getCount()
    print("Final count: \(final)")  // 应该是 10000
}

解析: 每个任务的 increment() 是 actor 的方法,actor 保证串行执行,所以即使并发也不会丢失数据。

练习 3: async/await vs Completion Handler 对比

把下面 Completion Handler 风格的函数改写成 async/await:

func oldStyleLoad(name: String, completion: @escaping (Data?) -> Void) {
    DispatchQueue.global().async {
        let url = Bundle.main.url(forResource: name, withExtension: "txt")!
        completion(try? Data(contentsOf: url))
    }
}
点击查看答案
func asyncLoadResource(name: String) async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        DispatchQueue.global().async {
            let url = Bundle.main.url(forResource: name, withExtension: "txt")!
            if let data = try? Data(contentsOf: url) {
                continuation.resume(returning: data)
            } else {
                continuation.resume(throwing: CocoaError(.fileReadCorruptFile))
            }
        }
    }
}

// 调用方
let data = try await asyncLoadResource(name: "test")
print("Loaded \(data.count) bytes")

解析: 用 withCheckedThrowingContinuation 把回调包装成 async 函数,调用方可以直接用 await


故障排查 FAQ

Q: async/await 和 GCD (Grand Central Dispatch) 有什么区别?

A: GCD 是基于线程的,你需要手动管理线程切换;async/await 是基于任务的(Task),系统自动调度:

// GCD 方式
DispatchQueue.global().async {
    let data = fetchData()
    DispatchQueue.main.async {
        updateUI(data)
    }
}

// async/await 方式
func load() async {
    let data = await fetchData()  // 自动返回主线程
    updateUI(data)  // 自动回到主线程
}

async/await 的优点:

  • 更少的线程切换:系统自动选择最优线程
  • 更好的错误处理:用 try/catch
  • 结构化并发:Task 有生命周期管理,不会"忘记"等待
  • 编译时安全:Swift 6.0 强制检查数据竞争

Q: Actor 和 class 有什么区别?

A:

  • class — 线程不安全。多个线程同时访问会导致数据竞争
  • actor — 线程安全。actor 内部的方法每次只能被一个任务调用
class UnsafeCounter {
    var count = 0
    func increment() { count += 1 }  // ❌ 多线程不安全
}

actor SafeCounter {
    var count = 0
    func increment() { count += 1 }  // ✅ 安全
}

简单记忆:需要线程安全时用 actor,其他用 class。

Q: Sendable 是什么?我的类型都需要实现它吗?

A: Sendable 是标记"可以在线程安全地传递的类型。

  • 值类型(struct、enum)— 天然 Sendable(每次传递是拷贝)
  • 引用类型(class、actor)— 需要显式标注 @Sendable 并保证线程安全
  • 只包含不可变数据的 struct — 自动符合 Sendable

Swift 6.0 模式下,跨线程传递非 Sendable 类型会编译报错。


小结

核心要点

  1. async/await 让异步代码可读性高 — 写起来像同步代码,但底层是异步的
  2. async let 实现并行 — 多个独立任务同时启动,最后 await
  3. Task 是并发基本单元 — 后台执行异步操作
  4. TaskGroup 管理大量任务 — 动态创建、自动等待
  5. Swift 6.0 编译时保证安全 — Sendable + actor 消灭数据竞争

关键术语

  • Async/Await: 异步编程关键字(标记异步函数和执行暂停)
  • Task: 并发任务(异步执行的基本单位)
  • Actor: 线程隔离类型(保证串行访问内部状态)
  • Sendable: 可安全跨线程传递(标记线程安全类型)
  • TaskGroup: 任务组(管理多个子任务的创建和等待)
  • AsyncSequence: 异步序列(流式数据传输)
  • Continuation: 延续(桥接回调到 async 的机制)

术语表

English中文
Async/Await异步等待
Task并发任务
TaskGroup任务组
Actor角色(线程安全类型)
@MainActor主线程隔离
Sendable可安全传递
Strict Concurrency严格并发(Swift 6.0)
AsyncSequence异步序列
Continuation延续
Data Race数据竞争
Structured Concurrency结构化并发
Completion Handler完成回调
DispatchSemaphore信号量

完整示例:Sources/BasicSample/ConcurrencySample.swift(asyncTaskSample / actorSample / batchAcotrSample / asyncStreamSample / simpleThreadSample)


知识检查

问题 1 🟢 (基础概念)

async 函数的调用必须在什么环境中?

A) 任何函数中直接调用
B) 另一个 async 函数或 Task 中
C) 主线程中
D) do-catch 块中

答案与解析

答案: B) 另一个 async 函数或 Task 中

解析: 异步函数只能在同样支持并发的上下文中调用。要么在另一个 async,要么用 Task 包装。直接在同步函数中调用 await 会导致编译错误。

问题 2 🟡 (设计决策)

有 10 个 HTTP 请求相互独立。如何最小化总耗时。应该用什么?

A) 顺序调用 10 次
B) 用 async let 同时发出
C) 用 10 个 Thread
D) 用 GCD 的 DispatchQueue

答案与解析

答案: B) 用 async let 同时发出

解析: 所有请求相互独立,用 async let 可以立即全部发出,总耗时至最多请求的时间。Thread/GCD 需要手动管理线程,async let 更简洁。

问题 3 🔴 (actor 隔离)

actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) {
        balance -= amount
    }

    func getBalance() -> Double {
        balance
    }
}

let account = BankAccount()
// 在 Task 中并发调用
await account.deposit(100)
await account.withdraw(30)
let bal = await account.getBalance()
print(bal)

bal 的值是什么?为什么是安全的?

答案与解析

答案: 70.0

解析: actor 内部的方法调用总是串行的,不会并发。所以 deposit 和 withdraw 不会同时执行,不存在数据竞争。await 等待方法完成后再获取结果,确保数据一致性。


延伸阅读

学完并发编程后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 错误处理闭包
  • 有经验开发者 → 探索 进阶 JSON 处理 中的 SwiftNIO 异步编程
  • 准备生产级应用 → 阅读 Swift 并发安全指南和 Sendable 要求

记住:Swift 的并发设计哲学是"让正确的做法变得简单"。async/await 让异步代码像同步代码一样易读,actor 让线程安全像类一样简单,Swift 6.0 让数据竞争在编译时就消失。这是现代并发编程的未来方向。


继续学习

  • 下一步:错误处理 — 异步操作中的错误处理模式
  • 相关:闭包 — 回调式并发 vs async/await
  • 进阶:SwiftNIO — 高性能网络服务的并发实践