2-21.【Concurrency】在 MainActor 上执行耗时任务会带来什么问题?如何优化?

0 阅读2分钟

1️⃣ 核心结论

在 MainActor 上执行耗时任务会阻塞主线程,导致 UI 卡顿、动画停滞和响应延迟。
优化方法是:将耗时任务移到后台线程执行(非 MainActor 上下文),完成后再切换回 MainActor 更新 UI。


2️⃣ 为什么会卡

MainActor 执行原理

  • MainActor executor 固定绑定 主线程
  • 同一时间 只能有一个任务执行
  • 所有 @MainActor 方法和属性访问都会排队执行

卡顿示例

@MainActor
func loadData() {
    // 耗时操作
    for i in 0..<1_000_000 { _ = i*i }
    
    label.text = "Done" // UI 更新
}

问题:

  • for 循环在主线程上执行 → 阻塞 executor
  • UI 无法刷新 → 应用卡死
  • 异步任务、动画和用户交互都被阻塞

3️⃣ 优化策略

① 移动耗时任务到后台

使用 非 MainActor context

Task.detached {
    let data = loadDataFromNetwork() // 耗时操作
    
    await MainActor.run {
        label.text = data
    }
}
  • Task.detached → 在后台线程执行
  • await MainActor.run { ... } → 回到主线程更新 UI
  • UI 不阻塞,响应流畅

② 使用 async/await + actor 分隔

actor DataLoader {
    func fetchData() async -> String {
        // 耗时操作,可以并发
        return await someNetworkCall()
    }
}

@MainActor
func updateUI() async {
    let loader = DataLoader()
    let data = await loader.fetchData() // 不阻塞主线程
    label.text = data
}
  • Actor 内部可并发执行耗时任务
  • MainActor 只负责 UI 更新
  • 保持主线程流畅

③ 小技巧

  1. 不要在 @MainActor async 函数里做 CPU 密集计算
  2. CPU 密集计算 → Task.detached / background queue
  3. I/O 或网络 → async/await 本身不会阻塞线程,但如果在 MainActor 包裹同步阻塞操作(如 FileManager.read)仍会卡

4️⃣ 类比理解

  • MainActor = “主线程队列 + 串行执行任务”
  • 耗时任务 = “一个大箱子阻塞队列”
  • 后果 = 所有 UI 和动画任务必须等待 → 卡顿
  • 解决方法 = “把大箱子搬到后台仓库,搬完再回到主线程更新”

5️⃣ 面试必背总结

  1. 在 MainActor 上执行耗时任务会阻塞主线程,导致 UI 卡顿、动画停滞和响应延迟。
  2. 优化方式:将耗时计算或 I/O 移到后台线程(Task.detached / 非 MainActor actor),完成后再 await MainActor.run 回到主线程更新 UI。
  3. 原理:MainActor executor 串行化任务,任何耗时操作都会阻塞 executor 的队列。