Visualize and optimize Swift concurrency
Swift & UI 进阶 20m

可视化与优化 Swift 并发

Visualize and optimize Swift concurrency

2022年6月6日

在 Apple 官方观看视频

一句话判断

Instruments 终于有了专门的 Swift Concurrency 模板——你可以直观地看到 actor 阻塞、线程池耗尽、continuation 泄露这些以前只能靠猜的并发问题。

这场 Session 讲了什么

Swift 的 async/await 和 actor 模型虽然大大简化了并发编程,但调试并发性能问题仍然是一个黑盒。这个 Session 介绍了 Xcode 14 中 Instruments 的新 Swift Concurrency 模板,让你能以可视化的方式追踪 Swift 并发的运行时行为。

Session 覆盖了几个典型的性能反模式:

  1. Main actor 阻塞——在 main actor 上执行耗时操作会导致 UI 卡顿,Instruments 现在能精确定位哪个 async 函数在 main actor 上停留了多久。
  2. Actor 争用——多个任务同时访问同一个 actor 时,某些任务会被挂起等待。Instruments 能展示这些等待时间的分布。
  3. 线程池耗尽——如果你在 cooperative thread pool 里跑了太多阻塞操作,新的 async 任务就无法被调度执行。
  4. Continuation 误用——withCheckedContinuation 如果忘记 resume,会导致任务永远挂起,Instruments 能帮你找到这些泄漏点。

值得深挖的点

Cooperative thread pool 的可视化是最大亮点。 在这之前,开发者几乎无法了解 Swift 并发的线程调度情况。你能看到每个时刻有多少线程在运行、有多少任务在排队、某个 actor 的等待队列有多长。这对于诊断”为什么我的 async 代码比同步代码还慢”这类问题非常关键。

Actor isolation 的性能影响。 Session 揭示了一个容易被忽视的问题:跨 actor 调用是有开销的。每次你从一个 actor 调用到另一个 actor,都涉及一次上下文切换。如果你的代码里有频繁的跨 actor 调用(比如在一个 for 循环里反复调用另一个 actor 的方法),性能损失会非常明显。解决方案是批量传递数据,减少跨 actor 边界的次数。

代码片段

// 典型的 actor 争用问题
actor DataCache {
    private var cache: [String: Data] = [:]
    
    func get(_ key: String) -> Data? {
        cache[key]
    }
    
    func set(_ key: String, data: Data) {
        cache[key] = data
    }
}

// 问题代码:频繁跨 actor 调用
func loadImages(urls: [URL], cache: DataCache) async throws -> [UIImage] {
    var images: [UIImage] = []
    for url in urls {
        // 每次循环都要跨 actor 边界两次(get + set)
        if let cached = await cache.get(url.absoluteString) {
            images.append(UIImage(data: cached)!)
        } else {
            let data = try await URLSession.shared.data(from: url).0
            await cache.set(url.absoluteString, data: data)
            images.append(UIImage(data: data)!)
        }
    }
    return images
}

// 优化方案:减少跨 actor 调用次数
func loadImagesOptimized(urls: [URL], cache: DataCache) async throws -> [UIImage] {
    let keys = urls.map { $0.absoluteString }
    var cached = await cache.batchGet(keys)  // 一次跨 actor 调用
    // ... 处理缺失的数据后一次性写入
    await cache.batchSet(newData)  // 一次跨 actor 调用
}

最佳实践

  • 用 Instruments 的 Swift Concurrency 模板做定期检查,不要等到用户报告卡顿才想起性能问题。
  • 检查 main actor 上的执行时间:任何超过几毫秒的 async 操作都不应该放在 main actor 上。
  • 注意 @MainActor 标注的传播性——一个标记了 @MainActor 的函数调用的其他函数也可能被隐式放到 main actor 上。
  • 使用 withCheckedContinuation 而不是 withUnsafeContinuation,前者会在 debug 模式下帮你检测遗漏的 resume 调用。

还有什么值得关注

  • Instruments 的 Swift Concurrency 视图支持按 actor 过滤,可以只看特定 actor 的活动。
  • 新增的 “Swift Tasks” instrument 可以看到每个 task 的创建、挂起、恢复和完成时间线。
  • 如果你使用了 TaskGroup,Instruments 能展示子任务之间的父子关系和执行顺序。
  • Session 建议在 Release 构建中也要做性能分析,因为某些并发开销在 Debug 和 Release 模式下差异很大。
WWDC 2022