1. 什么是线程安全?#
线程安全(Thread Safety) 指的是在多线程环境中,代码或数据结构可以被多个线程同时访问而不会产生 数据竞争(Data Race) 或 不可预期的行为。
- 线程安全的代码: 多个线程同时访问数据,结果仍然正确、稳定。
- 线程不安全的代码: 多个线程同时读写数据,可能导致崩溃、数据丢失或逻辑错误。
2. 线程安全常见场景#
✅ 2.1 并发任务的状态共享#
在 Swift 并发(async/await、Task、DispatchQueue 等)中,任务可能会在不同线程间切换,导致数据同时被多个任务访问。
var count = 0
Task {
count += 1 // 任务 A
}
Task {
count += 1 // 任务 B
}- 问题:
count是可变的,且没有保护机制,可能会导致数据竞争。 - 解决方案: 使用
DispatchQueue、NSLock或actor保护数据。
✅ 2.2 UI 更新#
在 SwiftUI 和 UIKit 中,所有 UI 更新都必须在主线程(Main Thread)执行。
DispatchQueue.global().async {
// ❌ 错误:在后台线程更新 UI
someUILabel.text = "Hello, world!"
}- 不安全: 直接在后台线程更新 UI,可能导致崩溃或渲染异常。
- 正确方式:
DispatchQueue.main.async {
someUILabel.text = "Hello, world!" // ✅ 主线程更新 UI
}在 SwiftUI 中使用 @MainActor 或 MainActor.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 的操作不是原子性的,易受中断影响。 | 计数器或累加器的并发更新。 |
| 在后台线程更新 UI | UI 操作不在主线程执行,导致崩溃或渲染异常。 | 在子线程直接更新 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、@MainActor、Sendable)已大幅简化线程安全的实现。 - 常见的线程不安全场景: UI 更新、共享可变数据、数据库操作、异步回调等。
- 确保线程安全的关键: 控制共享数据访问,使用合适的同步工具(如
DispatchQueue、NSLock、actor等)。