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
等)。