09 错误处理与并发

throws / try / Result 与 async / await / actor / Sendable 的完整速查

09 错误处理与并发

错误处理

Swift 用 throws 表示"这个函数可能失败",用 try 表示"我知道它可能失败"。这比异常好查,比返回码好读。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
enum ParseError: Error, Equatable {
    case empty
    case invalid(String)
}

struct Line {
    let x: Double, y: Double

    static func parse(_ text: String) throws -> Line {
        let parts = text.split(separator: ",")
        guard parts.count == 2 else { throw ParseError.invalid(text) }
        guard let x = Double(parts[0]), let y = Double(parts[1]) else {
            throw ParseError.invalid(text)
        }
        return Line(x: x, y: y)
    }
}

do {
    let line = try Line.parse("3,4")
    print(line.x + line.y)
} catch ParseError.empty {
    print("输入为空")
} catch let ParseError.invalid(text) {
    print("格式错误:\(text)")
} catch {
    print("其他错误:\(error)")
}
// prints: 7.0

五个关键字

关键字含义
throws函数声明可能抛错;错误类型默认是 any Error
try调用抛错函数时必须写,是给别人看的标记
try?失败就变成 nil,吞掉错误详情
try!失败就崩溃,只在测试或绝对不可能失败时用
rethrows只有闭包参数抛错时我才抛错
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func throwing() throws -> Int { 1 }

print((try? throwing()) as Any)
// prints: Optional(1)
print(try! throwing())
// prints: 1

// rethrows:只有当传进来的闭包会抛错时,这个函数才算会抛错
func run(_ body: () throws -> Void) rethrows { try body() }

try run { print(try throwing()) }
// prints: 1

catch 的四种抓法

catch 和 case 一样,能接各种模式。四种写法解决的问题分别是"是不是这一类"“是不是这个 case"“当成值拿来用"“再加个条件”:

写法抓什么拿到手的是什么
catch { }什么都抓error(any Error)
catch is MyError { }只抓某一类什么都不给,纯粹筛类型
catch MyError.timeout(let s) { }精确到一个 case关联值 s
catch let e as MyError { }某一类 + 绑定已转型的 e
catch let e where 条件 { }带条件的筛选e
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
enum NetError: Error { case timeout(Int), refused }

func run(_ n: Int) throws {
    if n == 1 { throw NetError.timeout(30) }
    if n == 2 { throw NetError.refused }
}

for n in 1...3 {
    do {
        try run(n)
        print("n=\(n) 成功")
    } catch is NetError {                       // 只判类型
        print("n=\(n) 是网络错误")
    } catch {
        print("其他")
    }
}
// prints:
//   n=1 是网络错误
//   n=2 是网络错误
//   n=3 成功

do { try run(1) } catch NetError.timeout(let s) { print("超时 \(s) 秒") }
// prints: 超时 30 秒

do { try run(2) } catch let e as NetError { print("当成值拿到:", e) }
// prints: 当成值拿到: refused

do { try run(1) } catch let e where e is NetError { print("带 where:", e) }
// prints: 带 where: timeout(30)

🔥 catch is MyError 是最省事的一种:不用起名字,也不想用这个错误,只想把它和别的错误分开处理。

defer:收尾清理

1
2
3
4
5
6
7
func cleanup() {
    defer { print("清理") }
    print("主体")
}
cleanup()
// prints: 主体
//         清理

⚠️ defer 的清理代码在离开作用域时执行,包括通过 throws 抛出的路径,但不包括 fatalError 和进程被杀。

类型化抛出 🆕

Swift 6 允许声明具体的错误类型,于是 catch 必须穷尽——和 switch 一样的待遇:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum NetError: Error { case timeout, refused }

func ping() throws(NetError) -> Int { throw .timeout }

do {
    _ = try ping()
} catch .timeout {
    print("超时")
} catch .refused {
    print("被拒绝")
}
// prints: 超时

🔥 类型化抛出把"这个函数到底会抛什么"写进了签名,调用方不用再靠猜或者 switch error as? ...。库的边界上值得用,内部小函数上写 throws 就够了。

Result:把错误当值

需要把成功或失败存起来、传出去、放进数组时,用 Result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 承接上文:enum ParseError 已定义
func parse(_ text: String) throws -> Int {
    guard let value = Int(text) else { throw ParseError.invalid(text) }
    return value
}

let ok: Result<Int, any Error> = Result { try parse("42") }
switch ok {
case .success(let v): print("成功 \(v)")
case .failure(let e): print("失败 \(e)")
}
// prints: 成功 42

let bad = Result { try parse("x") }
print((try? bad.get()) as Any)
// prints: nil

⚠️ Result(catching:) 对类型化抛出很挑:闭包抛出的类型必须与 Failure 完全一致,否则报 invalid conversion of thrown error type 'any Error' to 'ParseError'。注意这里比的是签名上的类型,不是"实际会抛什么”——上面那个 parse 只写 throws 时,它的错误类型就是 any Error,光给闭包加 throws(ParseError) 标注救不回来(报 thrown expression type 'any Error' cannot be converted to error type 'ParseError')。

两条路都行得通:让被调函数本身也类型化抛出,或者把 Failure 放回 any Error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 承接上文:enum ParseError、func parse(_:) throws -> Int 都已定义
func parseTyped(_ text: String) throws(ParseError) -> Int {
    guard let value = Int(text) else { throw ParseError.invalid(text) }
    return value
}

// 路一:函数与闭包的抛出类型都写成 ParseError
let typed: Result<Int, ParseError> = Result(catching: { () throws(ParseError) -> Int in
    try parseTyped("42")
})
print(typed)
// prints: success(42)

// 路二:被调函数只写 throws,那 Failure 就用 any Error
let loose: Result<Int, any Error> = Result { try parse("42") }
print(loose)
// prints: success(42)

给用户看的错误信息

同一件事——“读一个文件,可能失败”——三种风格各有适用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum FileError: Error { case missing(String) }

func read(_ path: String) throws -> String {
    guard path.hasSuffix(".txt") else { throw FileError.missing(path) }
    return "内容"
}

do {
    print(try read("a.txt"))
} catch {
    print("失败:\(error)")
}
// prints: 内容

适合"调用方必须处理、处理不了就继续往上抛"的场景。🔥

1
2
3
4
5
6
7
8
9
enum FileError: Error { case missing(String) }

func read(_ path: String) -> Result<String, FileError> {
    path.hasSuffix(".txt") ? .success("内容") : .failure(.missing(path))
}

let results = ["a.txt", "b.md"].map(read)
print(results.map { (try? $0.get()) ?? "—" })
// prints: ["内容", "—"]

适合"要收集一批结果、要存进数组、要稍后再处理"的场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum FileError: Error { case missing(String) }

func read(_ path: String) throws(FileError) -> String {
    guard path.hasSuffix(".txt") else { throw .missing(path) }
    return "内容"
}

do {
    print(try read("a.md"))
} catch .missing(let path) {
    print("找不到 \(path)")
}
// prints: 找不到 a.md

适合库的公开边界:调用方不用猜会抛什么,catch 还必须穷尽。

Error 的 localizedDescription 默认只说"操作无法完成”。要实现 LocalizedError 才有像样的话术:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 承接上文:enum ParseError 已定义
import Foundation

extension ParseError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .empty:               "输入为空"
        case .invalid(let text):   "无法解析:\(text)"
        }
    }
}

print(ParseError.invalid("abc").errorDescription ?? "")
// prints: 无法解析:abc

Never:一个值都没有的类型

Never 是标准库里一个空类型——它一个值都不存在。凡是"函数保证不会正常返回",返回值就写 Never:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum MyError: Error { case bad }

func fail(_ msg: String) -> Never {   // 永远不返回
    fatalError(msg)
}

func parse(_ s: String) throws -> Int {
    guard let n = Int(s) else { throw MyError.bad }
    return n
}

它真正好用的地方是给编译器递话。Result<Int, Never> 表示"这个操作要么成功、要么根本不该有失败分支",于是 switch 里只写 .success 就穷尽了——不需要 default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let alwaysOK: Result<Int, Never> = Result { 1 + 1 }
print(alwaysOK)
// prints: success(2)

func describe<T>(_ r: Result<T, Never>) -> T {
    switch r {
    case .success(let v): return v      // failure 分支不存在,不写也不报错
    }
}
print(describe(alwaysOK))
// prints: 2
场景写法
一定崩fatalError()、preconditionFailure() 返回 Never,所以能放在 guard 的 else 里
一定不返回自己写 -> Never 的函数,也能放在 guard 的 else 里
不会失败的结果Result<T, Never>,用 switch 时省掉 default
空的 switch对 Never 类型的值做 switch,一个 case 都不用写

⚠️ Never 不是"错误类型",别把 -> Never 当成"会抛错"的意思。它表达的是"这条路径压根回不来",fatalError 之外的进程退出函数也是这个类型。

并发的三条主线

Swift 6 的并发模型只有三个概念,理解了就能读懂 90% 的报错:

概念解决的问题
async / await不阻塞线程地等待
actor / @MainActor保护可变共享状态
Sendable标记"能安全跨线程传"的类型

async / await

异步函数用 async 标记,调用处用 await:

1
2
3
4
5
6
7
func fetchValue(_ id: Int) async -> Int {
    try? await Task.sleep(for: .milliseconds(10))
    return id * 10
}

print(await fetchValue(3))
// prints: 30

await 的意思是"这里可能挂起,让出线程去干别的",不是“开一个新线程”。Swift 的并发跑在协作式线程池上,任务数量可以远多于线程数。

⚠️ 只有 async 函数里才能 await。想在同步代码里进入异步世界,用 Task { }。

try、await、async let 的排列顺序

这几样经常挤在同一行,顺序是定死的:try 在前,await 在后。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func risky(_ n: Int) async throws -> Int { n * 2 }

func use() async throws {
    do {
        print(try await risky(1))
    } catch { print("失败") }

    print((try? await risky(2)) ?? 0)

    async let a = risky(3)          // 声明时不用写 try
    print(try await a)              // 取值时才 try await

    let stream = AsyncThrowingStream<Int, any Error> { c in
        c.yield(1); c.yield(2); c.finish()
    }
    var sum = 0
    for try await v in stream { sum += v }
    print(sum)
}
try await use()
// prints:
//   2
//   4
//   6
//   3
写法结果
try await f()✅ 标准顺序
try? await f() / try! await f()✅
await try f()⚠️ 实测只给警告:'try' must precede 'await'
async let x = f()✅ 声明处不写 try,取值处写 try await x
for try await x in seq✅ 同样 try 在前

⚠️ 和 ?? 一起用时还有一个更隐蔽的坑:try? 的作用范围比你想的大。

1
2
3
4
5
6
func f() throws -> Int { 1 }

print(try? f() ?? 0, type(of: try? f() ?? 0))
// prints: Optional(1) Optional<Int>
print((try? f()) ?? 0, type(of: (try? f()) ?? 0))
// prints: 1 Int

try? f() ?? 0 会被解析成 try? (f() ?? 0):?? 先跟 f() 结合,于是右边那个 0 永远用不上(编译器会警告 left side of nil coalescing operator '??' has non-optional type 'Int'),整个表达式的类型也仍然是 Int?。想表达"失败了就用 0",必须自己补一对括号。

并发执行:async let 与任务组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 承接上文:func fetchValue(_:) async -> Int 已定义
func parallel() async -> [Int] {
    async let a = fetchValue(1)
    async let b = fetchValue(2)
    return await [a, b]
}
print(await parallel())
// prints: [10, 20]

func groupSum() async -> Int {
    await withTaskGroup(of: Int.self) { group in
        for i in 1...3 {
            group.addTask { await fetchValue(i) }
        }
        var total = 0
        for await value in group { total += value }
        return total
    }
}
print(await groupSum())
// prints: 60
工具什么时候用
await f()顺序执行,一步等一步
async let固定数量、互不依赖的并行任务 🔥
withTaskGroup数量不定、结果要收集
withThrowingTaskGroup同上,但子任务会抛错
Task { }从同步代码里启动异步工作
Task.detached { }不继承当前上下文的任务,很少需要 🝖

⚠️ async let 的值该 await 就要 await。漏掉时会收到一条 initialization of immutable value 'a' was never used 的警告——注意它不是说任务被取消:子任务照样跑完,只是离开作用域时才被隐式等待、结果直接丢掉。所以白费的只有那个结果,代价却照付。

actor:保护可变状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
actor BankAccount {
    private(set) var balance: Int = 0
    func deposit(_ amount: Int) { balance += amount }
    nonisolated var describe: String { "一个银行账户" }
}

let account = BankAccount()
await account.deposit(100)
print(await account.balance, account.describe)
// prints: 100 一个银行账户

规则速记:

规则说明
外部访问可变状态要 await编译器替你排队,不会真的同时改
nonisolated明确声明"这个成员不碰内部状态",于是可以从任何地方同步调用
actor 内部可以直接互调不需要 await,因为已经在同一隔离域
actor 是引用类型传的是引用,不是拷贝
nonisolated 不等于"一定同步"它只免掉了隔离跳转;函数自己声明成 async 时照样要 await(见下)

⚠️ 上表最后一条是最容易记混的:要不要 await 看的是"这个函数是不是 async",不是"它是不是 nonisolated"。 三种组合实测如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
actor Store {
    nonisolated var label: String { "仓库" }              // 同步,直接读
    nonisolated func syncCount() -> Int { 0 }             // 同步,直接调
    nonisolated func asyncCount() async -> Int { 0 }      // 异步!必须 await
}

let store = Store()
print(store.label, store.syncCount())                     // 不需要 await
print(await store.asyncCount())                           // 🛑 漏掉 await 会报
// error: expression is 'async' but is not marked with 'await'

💭 为什么 nonisolated 还能是 async?因为"不碰隔离状态"和"会不会挂起"是两件事。nonisolated func f() async 不跳 actor、但可能真的等 I/O;它的执行器归属见下一节。

💭 actor 不是"加了锁的类"。它保证的是同一时刻只有一个任务在改它的状态,代价是调用点变成异步的。能用值类型解决的问题,别用 actor。

⚠️ actor 是可重入的:await 之后世界会变

这是 actor 最容易踩、也最不像"锁"的地方,值得单独一节:actor 保证"同一时刻只有一个任务在跑",但不保证"一个方法从头到尾不被打断"。 只要方法里出现 await,就是一次挂起点——actor 会放别的任务进来执行,等你回来时,状态可能早被改过了。

换句话说:actor 里的 await ≈ 主动让出锁。 判断"这段代码安全吗"的标准,和写并发代码时完全一样——挂起前读到的值,挂起后一律当作已经失效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
actor BankAccount {
    private(set) var balance: Int
    init(_ balance: Int) { self.balance = balance }

    // 🛑 错误写法:await 之前检查过一次,就以为稳了
    func buggyWithdraw(_ amount: Int) async -> Bool {
        guard balance >= amount else { return false }
        try? await Task.sleep(for: .milliseconds(10))   // ← 挂起点,别的任务趁虚而入
        balance -= amount                                // 余额可能早就被扣过了
        return true
    }

    // ✅ 正确写法:挂起之后**重新检查**,检查到扣款之间不能再有 await
    func withdraw(_ amount: Int) async -> Bool {
        guard balance >= amount else { return false }
        try? await Task.sleep(for: .milliseconds(10))
        guard balance >= amount else { return false }    // ⚠️ 这一行是关键
        balance -= amount
        return true
    }
}

两个任务同时从余额 100 的账户各取 100,实测结果(跑三遍都稳定):

1
2
3
4
=== 🛑 错误写法 ===
成功次数 2,余额 -100      ← 两个都以为自己是唯一取款人,账户被取穿
=== ✅ 正确写法 ===
成功次数 1,余额 0         ← 第二个任务在重查时被挡下

⚠️ 所以"把状态和方法都塞进 actor 就安全了"是错的。真正常用的三种写法:

做法说明
挂起后重查上面的 ✅ 版本;最直接,但要保证"检查 → 修改"之间没有 await
用一个 inFlight / 状态标记进入临界段前打标,退出时清掉;注意标记本身也要在挂起后重查
把整个操作做成同步方法根本不留挂起点,也就没有重入的余地——能用就最好

💭 用一句话概括:actor 防的是数据竞争,不防逻辑竞态(race condition 的那一半是"顺序",actor 不管)。 await 是个门,门一开,你之前读到的一切都需要重新确认。

@MainActor:把你按回主线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@MainActor
final class ViewModel {
    var count = 0
    func bump() { count += 1 }
}

func useViewModel() async {
    let vm = ViewModel()          // ⚠️ 无参 init 是 nonisolated 的,不用 await
    await vm.bump()
    print(await vm.count)
}
await useViewModel()
// prints: 1

⚠️ 这里有个反直觉的点,实测会给出警告 no 'async' operations occur within 'await' expression:成员都有默认值时,编译器合成的那个无参 init() 是 nonisolated 的,所以 await ViewModel() 属于白等一场。可一旦你手写了带参数的 init,它就被算作 actor 隔离的,必须 await:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@MainActor
final class VM2 {
    var count: Int
    init(count: Int) { self.count = count }   // 这个 init 是 main actor 隔离的
}

func useVM2() async {
    let vm = await VM2(count: 1)              // 参数化的 init 需要 await
    print(await vm.count)
}
await useVM2()
// prints: 1

💭 记法:“读/写隔离状态"才需要 await。 合成的无参 init 只是给属性填默认值,没碰任何需要隔离的东西,所以不必等;手写 init 里能访问隔离状态,于是它本身也被隔离了。拿不准就让编译器告诉你——多写一个 await 只会收到上面那条警告,少写一个才是错误。

@MainActor 可以标在类型、属性、方法上。UI 相关的一切都该在主线程,标上它以后,在别的线程碰它会直接编译报错——这比等到线上崩溃友好得多。

执行器:@concurrent 与 nonisolated(nonsending) 🆕

async 函数不是"一定在别的线程上跑”,它跑在某个执行器上。跑谁的执行器,取决于函数怎么声明:

写法跑在哪个执行器
普通同步函数调用方当前所在的地方
@MainActor func f()主 actor
nonisolated func f() async全局并发执行器(Swift 6 语言模式的默认行为)
@concurrent func f() async强制全局并发执行器 🆕
nonisolated(nonsending) func f() async强制跟着调用方 🆕

后两把扳手是 Swift 6.2 加的,专门用来消掉"我到底会不会被切走"这个疑问:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@concurrent
func heavy() async -> Int {
    // 不管谁调用,都切到全局并发执行器:CPU 密集型活儿放这里
    (1...1000).reduce(0, +)
}

nonisolated(nonsending)
func cheap() async -> Int {
    // 跟着调用方走:从 @MainActor 调就在主 actor 上,省掉一次线程跳转
    42
}

实测方法:写一个探针函数,函数体里调用 MainActor.assertIsolated()(不在主 actor 上就当场崩),再从 @MainActor 函数里调用它。

  • 标了 nonisolated(nonsending) 的探针通过——确实留在主 actor 上;
  • 把同一个函数改成普通 nonisolated,或标 @concurrent,断言当场崩(进程退出码 133)。

⚠️ 默认值还会继续变:Swift 6.2 提供了 NonisolatedNonsendingByDefault 这个 upcoming feature,打开之后普通 nonisolated async 函数也会跟着调用方,想强制切走就必须写 @concurrent——实测加上 -enable-upcoming-feature NonisolatedNonsendingByDefault 后,上面那个会崩的普通 nonisolated 探针就通过了。所以新代码里把"想不想被切走"写明确,比依赖默认值稳。

Sendable:能不能跨线程传

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import Synchronization

struct Config: Sendable { var name: String }   // 值类型,自动满足

final class Named: Sendable { let id = 3 }     // 只读 class,显式声明即可

final class Cache: @unchecked Sendable {       // 有可变状态,由你自己保证安全
    private let mutex = Mutex<[String: Int]>([:])
    func set(_ key: String, _ value: Int) { mutex.withLock { $0[key] = value } }
    func get(_ key: String) -> Int? { mutex.withLock { $0[key] } }
}

let cache = Cache()
cache.set("a", 1)
print(cache.get("a") ?? -1)
// prints: 1
类型是否 Sendable
struct,成员都是 Sendable✅ 自动推断
enum✅ 自动推断
final class,成员全是不变的 let⚠️ 不会自动推断,要显式写 : Sendable
class 有可变存储属性❌ 需要加锁 + @unchecked Sendable
闭包捕获的值都是 Sendable 时才算

⚠️ @unchecked Sendable 是你向编译器作出的承诺。加了这个标记却忘了加锁,编译器不会再替你检查——这也是为什么它和 nonisolated(unsafe) 一起,被归到"能合法写出数据竞争的少数几处"里。看到这两个词,就该有人去读一遍那段加锁代码。

1
nonisolated(unsafe) var globalCounter = 0   // 全局可变状态:隔离检查到此为止,安全性靠你自己

nonisolated(unsafe) 比 @unchecked Sendable 更直接:它把全局变量、静态属性的隔离检查整个关掉。它存在的意义是让老代码能一步一步搬进 Swift 6,不是给你省事的开关。

Mutex:新式的手动加锁 🆕

1
2
3
4
5
6
7
import Synchronization

let counter = Mutex(0)
counter.withLock { $0 += 1 }
counter.withLock { $0 += 1 }
print(counter.withLock { $0 })
// prints: 2

Mutex 来自 Swift 6 的 Synchronization 模块,比 NSLock 更轻,而且和 Sendable 配合得更干净。它要求 macOS 15 / iOS 18 以上。🚧

Atomic:连锁都不想加的时候 🆕

Synchronization 还带来了一组原子类型,适合"就一个整数计数"这种简单共享状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Synchronization

let counter = Atomic<Int>(0)
counter.add(1, ordering: .relaxed)
print(counter.load(ordering: .relaxed))
// prints: 1

let swapped = counter.compareExchange(expected: 1, desired: 20, ordering: .relaxed)
print(swapped.exchanged, swapped.original, counter.load(ordering: .relaxed))
// prints: true 1 20
操作写法
读a.load(ordering: .relaxed)
写a.store(10, ordering: .relaxed)
加 / 减a.add(1, ordering:)、a.subtract(1, ordering:)
比较并交换a.compareExchange(expected:desired:ordering:) → (exchanged:original:)
换一个新值、拿回旧值a.exchange(5, ordering:) → 旧值
只增不减 / 只取更大a.max(9, ordering:)、a.min(3, ordering:) → (oldValue:newValue:)
位运算改a.bitwiseOr(0b1010, ordering:)、bitwiseAnd、bitwiseXor 🝖

⚠️ Atomic 没有 withLock(实测报 value of type 'Atomic<Int>' has no member 'withLock')。它是"单个值上的原子操作",不是"一段代码的锁"——要保护多行逻辑、多个变量,请用 Mutex。

⚠️ ordering: 是内存序,不是"随便挑一个"。日常计数用 .relaxed 就够;要靠这个变量当发布信号(“我改完这行,别人就该看到前面那些改动”)时,才需要 .acquiring / .releasing / .acquiringAndReleasing 这些更强的顺序。不确定就用 Mutex,它的 withLock 语义直白得多。🚧

AsyncSequence:可以 await 的序列

1
2
3
4
5
6
7
8
9
let stream = AsyncStream<Int> { continuation in
    for i in 1...3 { continuation.yield(i) }
    continuation.finish()
}

var collected: [Int] = []
for await value in stream { collected.append(value) }
print(collected)
// prints: [1, 2, 3]

AsyncStream 是"把回调式 API 包成异步序列"的标准工具,处理通知、代理回调、socket 数据时特别顺手。

异步序列同样能接 where 过滤,和 for-in 一模一样;序列会抛错时,try 写在 for 后面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let stream = AsyncStream<Int> { continuation in
    for i in 1...5 { continuation.yield(i) }
    continuation.finish()
}

var odds: [Int] = []
for await value in stream where value % 2 == 1 {   // 只要奇数
    odds.append(value)
}
print(odds)
// prints: [1, 3, 5]
写法用在哪
for await x in seq不抛错的异步序列
for try await x in seq会抛错的异步序列(网络流、文件行读取)
for await x in seq where 条件边遍历边过滤

想在两个对象之间传递这条流(一个负责生产、一个负责消费)时,用 makeStream() 一次拿到"流 + 续写端":

1
2
3
4
5
6
7
8
9
let (stream, continuation) = AsyncStream<Int>.makeStream()
continuation.yield(1)
continuation.yield(2)
continuation.finish()          // 不调它,for await 会一直等下去

var got: [Int] = []
for await v in stream { got.append(v) }
print(got)
// prints: [1, 2]

会失败的流用 AsyncThrowingStream,结束时把错误一起交出去:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum Boom: Error { case go }

let (tstream, tcont) = AsyncThrowingStream<Int, Error>.makeStream()
tcont.yield(1)
tcont.finish(throwing: Boom.go)

do {
    var got: [Int] = []
    for try await v in tstream { got.append(v) }
    print(got)
} catch {
    print("流抛错了")
}
// prints: 流抛错了
需求写法
不抛错的流AsyncStream<Int> { continuation in ... } 或 AsyncStream<Int>.makeStream()
会抛错的流AsyncThrowingStream<Int, Error>.makeStream()、finish(throwing:)
结束continuation.finish();之后 yield 的值会被丢掉
限流makeStream(bufferingPolicy: .bufferingNewest(10))
缓冲策略.unbounded(默认,不丢但吃内存)、.bufferingNewest(n)、.bufferingOldest(n)

⚠️ AsyncStream 默认无限缓冲:生产者比消费者快多少,内存就涨多少。给通知、传感器这类"来得多、处理得慢"的数据源配上 .bufferingNewest(n),比事后查内存曲线便宜得多。

withCheckedContinuation:把回调式 API 接进 async

上一节是"多次回调",这一节是"一次回调"。老 API 基本都是"干完了叫我",想把它们变成 await,就用 continuation 接住那一次回调:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func loadValue(completion: @escaping @Sendable (Int) -> Void) {
    Task { completion(7) }        // 假装这是某个老 API 的异步回调
}

func loadValueAsync() async -> Int {
    await withCheckedContinuation { continuation in
        loadValue { value in
            continuation.resume(returning: value)     // 必须、且只能恢复一次
        }
    }
}

print(await loadValueAsync())
// prints: 7
工具什么时候用
withCheckedContinuation不会失败的回调
withCheckedThrowingContinuation会失败的回调:continuation.resume(with: result) 或 resume(throwing:)
withUnsafeContinuation想省掉运行期检查,只在性能敏感且确定写对时用 🝖

⚠️ continuation 必须恰好恢复一次。忘了恢复,任务就永远挂着(await 处再也不回来);恢复两次,Checked 版本会当场抓你——实测报 SWIFT TASK CONTINUATION MISUSE: twice() tried to resume its continuation more than once。这正是它比 Unsafe 版本多花一点开销换来的保护。

任务与取消

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let t = Task { () -> Int in
    try await Task.sleep(for: .milliseconds(5))
    return 7
}
print(try await t.value)
// prints: 7

t.cancel()
print(Task.isCancelled)
// prints: false     当前任务(顶层代码)没有被取消
做什么怎么写
启动任务Task { ... }
等结果try await task.value
取消task.cancel()(只是请求取消)
检查取消Task.isCancelled、try Task.checkCancellation()
睡一会儿try await Task.sleep(for: .seconds(1))
让出执行权await Task.yield(),别的任务有机会先跑
指定优先级Task(priority: .high) { ... }、Task.currentPriority
取消一整组任务组里的 group.cancelAll()
回主线程await MainActor.run { ... }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let hi = Task(priority: .high) { () -> String in
    "优先级 \(Task.currentPriority.rawValue)"
}
print(await hi.value)
// prints: 优先级 25

let ok = Task { () -> Int in
    await Task.yield()          // 主动让一步,别把执行器占死
    return 7
}
print(await ok.value)
// prints: 7

let slow = Task { () -> String in
    do {
        try await Task.sleep(for: .seconds(5))
        return "睡醒了"
    } catch {
        return "被取消:\(error is CancellationError)"
    }
}
slow.cancel()
print(await slow.value)
// prints: 被取消:true     cancel 会让正在 sleep 的任务立刻抛 CancellationError

⚠️ Task.currentPriority.rawValue 实测 .high 是 25,但这个数字是实现细节,别背:代码里认 .high、.userInitiated、.background 这些符号。

任务组的三种口味,按"要不要结果、会不会抛错"来选:

工具要不要结果会不会抛错典型场景
withTaskGroup要否固定格式的并行请求、并行计算
withThrowingTaskGroup要是子任务可能失败,失败要整体收摊
withDiscardingTaskGroup不要否长跑服务:每个连接起一个任务,跑完就扔 🆕
withThrowingDiscardingTaskGroup不要是同上,但子任务会失败(要写 try)🆕

⚠️ 四个名字里,“会不会抛错"取决于有没有 Throwing,和 Discarding 是两件独立的事。写错了编译器会说得很清楚:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
enum JobError: Error { case boom }

// 🛑 这个组不接会抛错的任务
await withDiscardingTaskGroup { group in
    group.addTask { throw JobError.boom }
}
// error: invalid conversion from throwing function of type '() throws -> Void'
//        to non-throwing function type '@isolated(any) () async -> Void'

// ✅ 换成 Throwing 版本,再补 try;错误会被抛到调用点
do {
    try await withThrowingDiscardingTaskGroup { group in
        group.addTask { throw JobError.boom }
    }
} catch {
    print("子任务炸了:", error)
}
// prints: 子任务炸了: boom
1
2
3
4
5
await withDiscardingTaskGroup { group in
    for i in 1...3 { group.addTask { _ = i * 2 } }   // 结果没人要,跑完就释放
}
print("组跑完")
// prints: 组跑完

💭 withDiscardingTaskGroup 的价值在内存:普通任务组会把每个子任务的结果攒到组结束才释放,长跑服务里这些结果会一直堆着;discarding 版本子任务一结束就把资源收掉。子任务的返回值必须是 Void,所以写法上就是"干活,不汇报”。

⚠️ 取消是协作式的。 cancel() 不会打断正在运行的代码,它只是设一个标志。你的循环要主动检查 Task.isCancelled,或者调用会抛 CancellationError 的 API。

withTaskCancellationHandler 就是给"取消那一刻"挂一个回调——比如把底层网络连接关掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func work() async {
    await withTaskCancellationHandler {
        try? await Task.sleep(for: .milliseconds(20))
        print("正常跑完")
    } onCancel: {
        print("收到取消请求,赶紧收尾")     // 在别的线程上立刻执行
    }
}
await work()
// prints: 正常跑完

💭 onCancel 里不能 await,它要能立刻返回——真正需要等待的清理放到后面的正常流程里做。

@TaskLocal:给任务打个"随行标签"

日志追踪里最常见的问题:十几个函数调用一层层传 requestID。@TaskLocal 让这个值跟着任务走,不用改任何函数签名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
enum Trace {
    @TaskLocal static var id: String = "无"
}

func handle() {
    print(Trace.id)                     // 作用域外是默认值
    Trace.$id.withValue("req-1") {
        print("内部:", Trace.id)         // 作用域内是被绑定的值
    }
    print("外部:", Trace.id)             // 出来又变回默认值
}
handle()
// prints:
//   无
//   内部: req-1
//   外部: 无
要点说明
声明方式@TaskLocal static var,必须带默认值
绑定方式Trace.$id.withValue(值) { ... },注意是 $ 那个投影
生效范围闭包内部,以及它 await 出去的所有子任务;出来自动还原
同步版本闭包不异步时不用写 await(实测加了 await 反而会警告 no 'async' operations occur within 'await' expression)
典型用途请求 ID、租户 ID、日志上下文 🔥

自定义全局 actor:不止有 @MainActor

@MainActor 是标准库给的一个全局 actor,你也可以自己造一个,把"必须串行"的那部分逻辑圈起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@globalActor
actor StoreActor {
    static let shared = StoreActor()
}

@StoreActor
final class Store {
    private(set) var rows: [String] = []
    func add(_ row: String) { rows.append(row) }
}

func load() async {
    let store = Store()                 // 创建不需要 await
    await store.add("a")                // 碰隔离状态才要
    print(await store.rows)
}
await load()
// prints: ["a"]

⚠️ 定义里那几行是固定套路:@globalActor + 一个 actor + 一个 static let shared 单例。名字写成别的(比如 static let instance)就会报 type 'StoreActor' does not conform to protocol 'GlobalActor'(实测)。

与 GCD 的对照

老代码里到处是 DispatchQueue。两者不是替代关系,但新代码基本不需要 GCD:

旧写法(GCD)新写法(Swift 并发)
DispatchQueue.global().async { }Task { }
DispatchQueue.main.async { }await MainActor.run { } 或 @MainActor
DispatchQueue.main.asyncAfter(deadline:)try await Task.sleep(for: .seconds(1))
DispatchGroupwithTaskGroup
DispatchSemaphore别用;阻塞线程会影响整个线程池 ⚠️
NSLockMutex(Swift 6)或 actor

⚠️ 绝对不要在 async 代码里用信号量或 sync 阻塞。 线程池的线程数是有限的(通常等于 CPU 核数),阻塞几个线程就可能让整个并发系统卡死,这种死锁极难复现。

陷阱速查

陷阱说明
try? 吞掉错误详情排查问题时信息全丢,能 do-catch 就别偷懒
try! 用在不可控输入上崩溃的经典来源
catch 顺序写错具体类型要放在通用的 catch 之前
defer 里抛错错误出不去:defer { try f() } 报 call can throw, but errors cannot be thrown out of a defer body;要在里面调会抛错的函数,得用 try? 或自己 do-catch 收掉
Result(catching:) 与类型化抛出抛出类型必须与 Failure 严格一致
用 Task { } 忘记 await 结果任务会照常执行,但错误被静默丢弃
async let 漏掉 await只会收到 never used 警告,子任务照样跑完,丢的是结果不是计算
在 async 函数里阻塞线程要等就 try await Task.sleep(for:);Thread.sleep(forTimeInterval:)(Foundation)和信号量会把线程占住不放,拖垮线程池
@unchecked Sendable 当免死金牌它只是"我保证",编译器不再检查
以为 cancel() 会立刻停下取消是协作式的,需要自己检查
在同步函数里访问 @MainActor 属性编译报错,要 await 或把函数标成 @MainActor
以为进了 actor 就万事大吉actor 是可重入的:每个 await 都是一个让别的任务插进来的口子,挂起前读到的值挂起后要重查
最后修改 September 20, 2026: 更新 (25684a4ed)