2-28.【Concurrency】async/await 与 GCD 混用时需要注意哪些线程安全问题?

1 阅读2分钟

1️⃣ 核心原则

async/await 不自动保证线程安全,GCD 也只是队列调度工具。
混用时,要明确 任务执行上下文数据访问隔离,否则容易出现竞态条件或 UI 卡顿。

关键点:

  • Actor(包括 MainActor)保证内部状态安全
  • GCD 队列本身不能保护跨队列共享状态
  • async/await 任务可能切换线程

2️⃣ 常见线程安全问题

① 非 Actor 对象跨线程访问

var count = 0

Task {
    count += 1 // 可能在某个线程执行
}

DispatchQueue.global().async {
    count += 1 // 并发访问 count → 数据竞争
}
  • count 被 async Task 和 GCD 同时修改
  • 可能发生数据竞争 → 崩溃或不可预测行为

② MainActor 与 GCD.main 混用

@MainActor
var labelText: String = ""

DispatchQueue.global().async {
    labelText = "Hello" // ❌ 非主线程直接修改 UI
}
  • 违反 MainActor 数据隔离 → 编译器报错(如果是 @MainActor 修饰)
  • 若未使用 Actor,则可能在后台线程修改 UI → 崩溃

③ 多线程共享资源未同步

  • 对共享数组、字典等在 async Task 和 GCD 中同时访问
  • 必须加锁或使用 Actor,否则可能越界、重复或丢失元素

3️⃣ 安全使用策略

① 使用 Actor 或 MainActor 保护状态

actor Counter {
    var count = 0
    func increment() { count += 1 }
}

let counter = Counter()

Task {
    await counter.increment() // 安全
}

DispatchQueue.global().async {
    Task {
        await counter.increment() // 也安全
    }
}
  • Actor 内部串行化访问 → 避免竞态
  • 跨线程调用使用 await,保证安全

② UI 操作必须回到主线程 / MainActor

Task {
    let data = await fetchData()
    await MainActor.run { 
        label.text = data
    }
}
  • 即使 Task 在后台线程完成数据处理,也要回主线程更新 UI
  • 避免 DispatchQueue.main.async 与 async/await 混用造成竞态

③ 共享资源多线程访问

  • 使用 ActorDispatchQueue + barrier

Actor 保护示例

actor SafeArray {
    private var items: [Int] = []
    func append(_ value: Int) { items.append(value) }
    func getAll() -> [Int] { items }
}

GCD barrier 示例

let queue = DispatchQueue(label: "com.example.array", attributes: .concurrent)
var array: [Int] = []

queue.async(flags: .barrier) {
    array.append(1)
}

④ 避免在 CPU 密集循环中阻塞主线程

@MainActor
func heavyWork() {
    for i in 0..<1_000_000 { 
        // ❌ CPU 密集,阻塞主线程
    }
}
  • async Task 或 GCD.global() 执行耗时操作
  • 更新 UI → await MainActor.run {}

⑤ 明确 async Task 与 GCD 执行上下文

  • Task { } 默认继承父任务上下文 → 不固定线程
  • Task.detached { } → 随机线程
  • DispatchQueue.async → 指定队列
  • 注意不要假设 async Task 永远在主线程执行

4️⃣ 总结建议

  1. 共享状态 → 用 Actor 或锁保护
  2. UI 更新 → MainActor.run 或 @MainActor async 函数
  3. 耗时操作 → 在后台线程执行,不阻塞主线程
  4. Task 与 GCD 混用 → 明确线程和上下文
  5. 协作式取消 → 在后台 Task 中定期检查 Task.checkCancellation()

5️⃣ 面试必背结论

  1. async/await 任务可能在任意线程执行,GCD 调度也可能在不同线程。
  2. 共享资源必须保证线程安全,否则会发生数据竞争。
  3. Actor / MainActor 是 async/await 推荐的线程安全隔离机制。
  4. UI 必须回主线程,耗时操作必须在后台。
  5. 混用时要明确任务上下文,避免假设线程一致性。