并发编程 — 线程安全

1. 什么是线程安全? #

线程安全(Thread Safety) 指的是在多线程环境中,代码或数据结构可以被多个线程同时访问而不会产生 数据竞争(Data Race)不可预期的行为

  • 线程安全的代码: 多个线程同时访问数据,结果仍然正确、稳定。
  • 线程不安全的代码: 多个线程同时读写数据,可能导致崩溃、数据丢失或逻辑错误。

2. 线程安全常见场景 #

2.1 并发任务的状态共享 #

在 Swift 并发(async/awaitTaskDispatchQueue 等)中,任务可能会在不同线程间切换,导致数据同时被多个任务访问。

var count = 0

Task {
    count += 1   // 任务 A
}

Task {
    count += 1   // 任务 B
}
  • 问题: count 是可变的,且没有保护机制,可能会导致数据竞争。
  • 解决方案: 使用 DispatchQueueNSLockactor 保护数据。

2.2 UI 更新 #

SwiftUIUIKit 中,所有 UI 更新都必须在主线程(Main Thread)执行。

DispatchQueue.global().async {
    // ❌ 错误:在后台线程更新 UI
    someUILabel.text = "Hello, world!"
}
  • 不安全: 直接在后台线程更新 UI,可能导致崩溃或渲染异常。
  • 正确方式:
DispatchQueue.main.async {
    someUILabel.text = "Hello, world!"  // ✅ 主线程更新 UI
}

在 SwiftUI 中使用 @MainActorMainActor.run 可确保线程安全:

@MainActor
func updateUI() {
    someUILabel.text = "Hello, world!"
}

2.3 数据库操作 #

当多个线程同时读写数据库(如 Core Data 或 SQLite),容易出现数据不一致或崩溃。

let context = persistentContainer.viewContext

DispatchQueue.global().async {
    context.save()  // ❌ 在多个线程同时访问 context
}
  • 问题: NSManagedObjectContext 不是线程安全的。
  • 解决方案: 为每个线程使用独立的 context,或使用线程安全的数据库队列。

2.4 网络请求与回调 #

异步网络请求的回调可能在后台线程执行,直接处理 UI 或共享数据会导致线程不安全。

URLSession.shared.dataTask(with: url) { data, response, error in
    self.sharedData.append(data!)  // ❌ 不安全的数据访问
}.resume()
  • 解决方案: 使用 DispatchQueue.main.async 切换回主线程:
URLSession.shared.dataTask(with: url) { data, response, error in
    DispatchQueue.main.async {
        self.sharedData.append(data!)  // ✅ 安全处理
    }
}.resume()

3. 线程不安全的常见原因 #

原因描述示例
同时读写可变数据多个线程同时修改同一变量,导致数据竞争。共享数组或字典未加锁保护。
非原子操作类似 x = x + 1 的操作不是原子性的,易受中断影响。计数器或累加器的并发更新。
在后台线程更新 UIUI 操作不在主线程执行,导致崩溃或渲染异常。在子线程直接更新 UILabel
非线程安全的 API 调用调用不支持多线程的第三方库或 API,导致异常。Core Data 的 NSManagedObject
引用类型的共享多个线程共享同一对象实例,且未进行同步控制。引用类型类对象未使用锁。

4. 如何确保线程安全? #

4.1 使用 DispatchQueue 控制并发 #

使用串行队列(serial queue)或 DispatchQueue.sync 控制并发访问。

let queue = DispatchQueue(label: "com.example.queue")

queue.async {
    // ✅ 串行队列确保线程安全
    sharedData.append("New Data")
}

4.2 使用 NSLock 加锁 #

简单有效的线程安全工具,适用于需要手动控制临界区的场景。

let lock = NSLock()
var sharedData = [String]()

func threadSafeAppend(_ value: String) {
    lock.lock()
    sharedData.append(value)  // ✅ 加锁保护
    lock.unlock()
}
  • 优点: 易于理解和实现。
  • 缺点: 不当使用可能导致死锁或性能瓶颈。

4.3 使用 actor(Swift Concurrency) #

Swift 提供的 actor 是一种专门为并发设计的类型,自动保证其内部状态的线程安全。

actor Counter {
    private var value = 0

    func increment() {
        value += 1  // ✅ 自动线程安全
    }

    func getValue() -> Int {
        return value
    }
}

let counter = Counter()

Task {
    await counter.increment()
    print(await counter.getValue())
}
  • 优点: 语法简洁,自动处理线程安全。
  • 缺点: 适用于简单的状态管理,复杂场景下可能需要额外优化。

4.4 使用 @MainActor 确保 UI 线程安全 #

@MainActor 确保方法或属性只在主线程上运行,适用于 UI 更新场景。

@MainActor
class ViewModel: ObservableObject {
    @Published var text: String = ""

    func updateText() {
        text = "更新完成"  // ✅ 自动在主线程执行
    }
}

调用时无需手动切换线程,系统会自动调度到主线程。


5. 总结 #

  • 线程安全 = 确保数据在多线程并发访问时不出现竞争条件。
  • Swift 并发模型(如 actor@MainActorSendable)已大幅简化线程安全的实现。
  • 常见的线程不安全场景: UI 更新、共享可变数据、数据库操作、异步回调等。
  • 确保线程安全的关键: 控制共享数据访问,使用合适的同步工具(如 DispatchQueueNSLockactor 等)。
本文共 1494 字,创建于 Feb 5, 2025
相关标签: Swift, Xcode