无挂起的异步方法调用链中如何避免线程切换?
问题场景
你给出的代码里,AppCoordinator标记了@MainActor,它的start方法调用settings.write这个异步函数时,明明write内部没有任何挂起操作,运行时还是会把执行转移到其他线程,平白增加性能开销。而且我们还不能随便修改协议的async throws签名,也不想硬给write绑定@MainActor(毕竟协议的实现可能不止UserDefaults)。
解决办法
1. 用@inheritsExecutor标记协议方法(推荐,Swift 5.7+)
这是Swift官方支持的特性,直接在协议的异步方法上加上@inheritsExecutor,就能让所有实现该方法的代码继承调用者的执行上下文,不会随便切换线程:
protocol Settings { @inheritsExecutor func write(bool: Bool, atKey key: String) async throws } extension UserDefaults: Settings { func write(bool: Bool, atKey key: String) async throws { set(bool, forKey: key) } }
这个属性的作用很明确:告诉编译器,这个异步函数不需要切换执行器,只要内部没挂起点,就直接在调用者的线程上跑,完美适配你的场景。
2. 给实现方法加@_unsafeInheritExecutor(Swift 5.7+)
如果不想修改协议,也可以直接在UserDefaults的扩展方法上用这个属性:
extension UserDefaults: Settings { @_unsafeInheritExecutor func write(bool: Bool, atKey key: String) async throws { set(bool, forKey: key) } }
注意:这个属性带下划线,属于未公开的“私有”API,虽然很多开发者在实际项目里用,但Apple未来可能会调整它的行为,用的时候要权衡风险。
3. 显式在当前Actor上下文执行
如果不想依赖上面的属性,也可以在write里手动把操作拉回调用者的Actor上下文。比如这里调用者是MainActor,就这么写:
extension UserDefaults: Settings { func write(bool: Bool, atKey key: String) async throws { await MainActor.run { set(bool, forKey: key) } } }
不过不用担心,Swift运行时会优化这种情况——如果当前已经在MainActor上下文里,MainActor.run会直接执行代码,不会真的触发线程切换。
原理说明
默认情况下,Swift的异步函数会被调度到全局并发池执行,哪怕函数里没有挂起点。而上面的属性就是告诉编译器:别瞎调度,就用调用者的执行上下文跑。只要函数内部没有await之类的挂起点,就不会发生线程切换,性能损耗自然就没了。
要是函数里有挂起点,那该切换还是会切换,毕竟挂起后要等结果,运行时会合理调度线程,这时候的切换就是必要的了。
内容的提问来源于stack exchange,提问作者Cristik

