2-29.【Concurrency】在混用 MainActor 与 DispatchQueue 的场景下,如何避免死锁或线程阻塞?

5 阅读2分钟

1️⃣ 核心结论

MainActor + DispatchQueue 混用时,死锁通常源于:

  1. 主线程被同步阻塞(DispatchQueue.main.sync {}
  2. async/await 在主线程挂起等待自身执行完成

避免方法:

  • 避免在主线程使用同步队列调用(sync
  • 使用 await MainActor.run {} 或异步 DispatchQueue.main.async
  • 耗时操作切到后台线程

一句话:主线程永远不要被同步阻塞或等待它自己完成异步任务。


2️⃣ 常见死锁场景

① 同步阻塞主线程

DispatchQueue.main.sync {
    Task {
        await updateUI()
    }
}
  • 问题:

    1. 主线程被 sync 阻塞
    2. Task 在 MainActor(主线程)排队执行 → 永远无法执行
  • 结果 → 死锁


② async/await 自己等待自己

@MainActor
func updateUI() async {
    await updateUI() // ❌ 自己等待自己
}
  • Task 在主线程排队执行
  • await 让 Task 挂起
  • 没有其它线程执行 → 死锁

3️⃣ 正确模式

① 使用 await MainActor.run {} 或异步 Task

Task.detached {
    // 后台线程执行耗时任务
    let data = await fetchData()

    await MainActor.run {
        // 回主线程更新 UI
        label.text = data
    }
}
  • Task.detached → 后台线程
  • MainActor.run → 异步切换回主线程
  • 不阻塞主线程 → 避免死锁

② 避免同步 DispatchQueue.main.sync

  • 不要用 sync 调度到主线程等待结果
  • 用异步方式:
DispatchQueue.main.async {
    label.text = "Hello"
}
  • 或用 await MainActor.run {}
  • 两者都不会阻塞调用线程

③ 耗时任务切到后台

@MainActor
func updateUI() async {
    let result = await Task.detached {
        computeHeavyTask()
    }.value

    label.text = "(result)"
}
  • CPU 密集任务不在主线程
  • 主线程只负责 UI 更新 → 保持响应流畅
  • async/await 保证线程安全,不用锁

④ 小心 TaskGroup 与 MainActor

await withTaskGroup(of: Void.self) { group in
    group.addTask {
        await Task.sleep(1_000_000_000)
        await MainActor.run { print("UI update") }
    }
}
  • TaskGroup 子任务在后台执行
  • UI 更新通过 MainActor.run
  • 避免在 MainActor 上直接等待 TaskGroup 内子任务完成 → 防止主线程阻塞

4️⃣ 死锁排查思路

  1. 主线程是否被阻塞?

    • DispatchQueue.main.sync
    • MainActor.run 的外层同步调用
  2. 异步任务是否排队自己等待自己?

  3. CPU 密集任务是否在主线程执行?

    • 长循环或同步 I/O

5️⃣ 面试必背总结

  1. 不要在主线程使用 DispatchQueue.main.sync,永远用 async 或 await
  2. 耗时任务切后台,UI 更新回主线程
  3. 避免 async 函数自己等待自身 → 死锁
  4. MainActor.run 异步切换 + Task.detached / TaskGroup 后台执行 → 安全模式
  5. 检查 Task 排队顺序和挂起点,确保主线程不会被阻塞