Swift 6.2 的 Approachable Concurrency 如何迁移并发代码?
1. 题目场景与约束
你维护一个图片编辑 App。缓存由 UI 访问,网络下载、解码和缩略图生成耗时较长。旧代码依赖隐式线程切换,部分 API 是 nonisolated async,测试只覆盖最终图片是否正确。现在要采用 Swift 6.2,并保持滚动流畅、结果可复现,同时逐步打开 Swift 6 的数据竞争检查。
请先说明你会记录哪些基线:主线程帧时间、解码吞吐、并发任务数、缓存命中率、取消延迟,以及迁移前后的编译器诊断数量。没有这些基线,性能回归和“修复”很难区分。
2. 先解释 Swift 6.2 改变了什么
Swift 6.2 提供可选的 -default-isolation MainActor,让目标中的代码默认隔离到主 actor;它还引入 @concurrent,显式标记需要并行执行的函数。启用 NonisolatedNonsendingByDefault 后,nonisolated async 默认沿用调用者的 actor,上下文不会自动跳到全局并发执行器;需要保留并行语义时再用 @concurrent。
这些选项降低了注解噪音,却没有让所有工作自动并行。默认隔离只描述谁能安全访问状态,@concurrent 才表达“这里允许离开当前 actor 并行运行”。
3. 设计隔离边界
把可变缓存和 UI 状态放在 @MainActor 类型中,把纯值转换和不可变输入输出的解码函数设计为 Sendable。下载层只返回值类型,避免把带有隐式线程亲和性的对象跨 actor 传递。每个 API 都写清楚三件事:调用者 actor、被保护的状态、是否允许并行。
例如缓存可以保持主 actor 保护:
@MainActor
final class ImageCache {
private var values: [URL: Image] = [:]
func value(for url: URL) -> Image? { values[url] }
func insert(_ image: Image, for url: URL) { values[url] = image }
}不要用 MainActor.run 到处“包住”旧代码来消除警告;迁移指南把它定位为过渡工具,长期应在 API 类型和签名上表达隔离要求。
4. 何时使用 @concurrent
只有当任务可以安全地并行执行、输入输出满足发送要求且基准证明并行有收益时,才给函数加 @concurrent。图片解码通常符合这个条件:它不直接读写主 actor 缓存,返回新建的值。
@MainActor
struct ImagePipeline {
static func image(from url: URL) async throws -> Image {
let cached = cache.value(for: url)
if let cached { return cached }
let image = try await decode(url: url)
cache.insert(image, for: url)
return image
}
@concurrent
private static func decode(url: URL) async throws -> Image {
let (data, _) = try await URLSession.shared.data(from: url)
return try ImageDecoder.decode(data)
}
}面试时要指出:@concurrent 不是“更快”开关。过度使用会增加调度、内存和同步成本;若解码库本身串行或图片很小,吞吐可能下降。
5. 处理调用者上下文与 Sendable
启用调用者上下文语义后,一个 async 函数可能继续在调用者 actor 上运行。它适合访问调用者隔离的状态,但长时间 CPU 工作会阻塞该 actor,因此应拆出 @concurrent 的纯计算阶段。跨 actor 的参数和结果必须是 Sendable,或由 actor/锁等同步边界保护。
不要通过 @unchecked Sendable 静音所有诊断。只有在你能证明内部不变量、同步原语和生命周期后,才把它作为窄范围的最后手段,并配套并发压力测试。
6. 分阶段迁移与工具链
先在单个模块启用即将到来的特性和严格诊断,利用迁移工具生成警告与 fix-it;再把公共 API 的隔离和 Sendable 约束稳定下来,最后切换 Swift 6 语言模式。每阶段都保留可回滚的提交,并把编译器诊断按模块归档。
迁移顺序建议是:值类型与协议约束、底层服务、缓存 actor、UI 调用层。这样错误会在边界处暴露,避免一次改动数百个 await 后无法定位。旧客户端需要兼容时,可先维护 Swift 5 模式的接口,再逐步提高实现模块的检查级别。
7. 如何验证没有竞争或意外串行化
功能测试之外,加入并发压力:同时请求同一 URL、随机取消、重复写入和跨 actor 读取;用 Thread Sanitizer 与严格并发诊断捕获竞态。性能测试分别覆盖冷缓存、热缓存、不同图片尺寸和不同并发上限,比较帧时间、吞吐、峰值内存与取消延迟。
检查任务上下文和异步调用栈,确认解码阶段确实并行、缓存读写仍在主 actor。若开启 @concurrent 后帧时间没有改善,优先检查解码器是否阻塞、任务是否被单一锁串行化,以及是否产生过多短任务。
8. 评分要点与追问
必须说清
- 区分默认隔离、调用者上下文和显式并行;不把
async等同于后台线程。 - 以
Sendable、actor 边界和可取消性证明安全,而不是靠@unchecked Sendable消除错误。 - 给出分阶段迁移、压力测试和性能基线,能解释回归信号。
常见追问
- 如果解码库不是线程安全的,你会把它放在 actor、串行 executor 还是进程外服务?为什么?
- 如何限制同一 URL 的重复下载,同时不让整个缓存串行?
- 迁移期间 Swift 5 与 Swift 6 模块如何共享 API,哪些约束必须先发布?
评分参考
优秀答案能把隔离建模、迁移工具和运行时证据连成闭环:先定义状态与执行上下文,再用最小的 @concurrent 提升并行度,最后用编译器、Sanitizer 和基准数据验证结论。