并发编程 — Sendable

1. 什么是 Sendable #

SendableSwift 并发模型 引入的一个协议,用于确保类型在多个线程或并发上下文中传递时是 线程安全 的。它的核心作用是:

  • 标记数据是否可以安全地在任务、线程或 Actor 之间共享。
  • 防止数据竞争条件,提升代码的并发可靠性。
struct User: Sendable {
    let name: String
    let age: Int
}

在这个例子中,User 结构体是不可变的(let 属性),因此它天然是线程安全的,符合 Sendable 要求。


2. 为什么需要 Sendable #

在 Swift 并发中,任务可能会在不同线程之间调度。为了避免数据竞争和内存访问冲突,编译器需要确保:

  • 数据在传递到不同并发上下文时是安全的。
  • 确保 Actor 的内部状态不会因为并发访问而出现竞态条件。

如果未正确标记或实现 Sendable,编译器会在并发代码中发出警告或错误,帮助开发者提前发现潜在的线程安全问题。


3. Sendable 的使用场景 #

✅ 3.1 在 Actor 中共享数据 #

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }
}

struct Logger: Sendable {
    let message: String
}

let counter = Counter()

Task {
    await counter.increment()
    print(Logger(message: "计数器已增加"))
}
  • Logger 是不可变的,因此可以标记为 Sendable,安全地跨任务使用。
  • actor 会自动保护内部状态,但与外界的数据交互时仍需确保数据是 Sendable

✅ 3.2 在 Task 中传递数据 #

struct DataModel: Sendable {
    let id: Int
    let name: String
}

let data = DataModel(id: 1, name: "并发数据")

Task.detached { 
    print(data)  // ✅ 安全,因为 DataModel 符合 Sendable
}
  • Task.detached 是独立于当前上下文的任务,尤其强调线程安全。
  • 编译器会强制要求传入的数据是 Sendable,否则会警告。

✅ 3.3 自定义类的线程安全 #

final class Config: Sendable {
    private let lock = NSLock()
    private var _value: Int = 0

    var value: Int {
        get {
            lock.lock()
            defer { lock.unlock() }
            return _value
        }
        set {
            lock.lock()
            _value = newValue
            lock.unlock()
        }
    }
}
  • 类默认不符合 Sendable,因为类实例通常是引用类型,可能导致数据竞争。
  • 如果要标记为 Sendable,必须自己确保线程安全,比如使用 NSLock 保护。

4. 何时必须使用 Sendable #

场景是否必须 Sendable解释
Actor 内部状态共享✅ 必须防止竞态条件,确保数据安全。
Task.detached 中的数据传递✅ 必须因为任务脱离父上下文,线程安全尤为重要。
跨线程传递数据✅ 必须保证数据在不同线程间的安全。
简单的 Taskasync 函数❌ 不强制,但推荐如果没有跨线程访问,编译器不会强制要求。
引用类型(类)❌ 默认不符合,需手动确保线程安全需使用锁机制或设计为不可变类。

5. Sendable 自动符合的类型 #

Swift 中很多基础类型天然符合 Sendable,因为它们是不可变或具备值语义:

  • 原始数据类型: IntDoubleBoolString
  • 集合类型: ArrayDictionary(前提是元素也符合 Sendable
  • 枚举类型: 如果所有关联值都符合 Sendable,则枚举自动符合
  • 结构体: 所有属性都符合 Sendable,则结构体自动符合

6. 自定义类型如何手动符合 Sendable #

✅ 简单结构体自动符合: #

struct Point: Sendable {
    let x: Int
    let y: Int
}

因为 xy 都是 Int,自动符合 Sendable


⚠️ 引用类型手动实现: #

final class Counter: @unchecked Sendable {
    private var value = 0

    func increment() {
        value += 1  // 需要开发者确保线程安全
    }
}
  • 使用 @unchecked Sendable 表示你承诺线程安全,编译器不再强制检查。
  • 风险: 开发者需要自行确保内部实现的线程安全,否则可能导致隐藏的竞态条件。

7. 常见问题与陷阱 #

1. 为什么我的代码会提示 Sendable 警告? #

  • 可能因为你在并发上下文中传递了一个不符合 Sendable 的类型,比如引用类型的对象。
  • 解决方案:检查数据结构,确保其属性是不可变的,或使用锁保护可变状态。

2. 一定要加 Sendable 吗? #

  • 不一定。 如果代码只在单线程或没有跨并发边界,Sendable 并不是强制的。
  • 但推荐: 在任何可能的并发场景下使用 Sendable,即使编译器不报错,这是一种良好的编程习惯。

3. 为什么类默认不符合 Sendable #

  • 因为类是引用类型,可能被多个任务共享,容易产生竞态条件。
  • 如果要标记为 Sendable,必须明确确保线程安全,比如使用 NSLockDispatchQueue 等机制。

8. 总结 #

  • Sendable 是 Swift 并发的线程安全基石。
  • 在并发场景下,确保数据可以安全地在任务、线程和 Actor 之间传递。
  • 使用 @unchecked Sendable 需要格外小心,确保线程安全。
  • 推荐在设计并发模型时,尽量使用不可变的数据结构,这样天然符合 Sendable,减少出错概率。
本文共 1614 字,创建于 Feb 5, 2025
相关标签: Swift, Xcode