Swift异步编程:如何正确使用await避免竞态与主线程阻塞
非SwiftUI代码库中async/await的常见疑问解答
核心疑问先明确
在
@MainActor上执行await是否错误?
完全不是错误。await的本质是暂停当前任务并让出线程,而非阻塞线程。只要await后面的异步操作本身是非阻塞的(比如网络请求、异步IO),主线程在等待期间可以正常处理UI事件、刷新界面,不会导致卡顿。在Task或其他Actor内部访问属性是否错误?
不一定,关键看属性的线程安全性:- 如果属性是不可变常量(比如
let声明的stuffLoader),跨线程访问没问题; - 如果属性是可变变量(比如
var param),且没有任何同步机制(比如Actor隔离、锁),跨线程读写就会引发竞态条件,这才是错误的。
- 如果属性是不可变常量(比如
针对三个方法的具体分析
1. loadStuff1是否会引发竞态条件?正确做法是什么?
风险分析:
stuffLoader是let常量,初始化后不会修改,无论在哪个线程访问都安全;param是var变量,假设它的修改操作都在主线程(比如UI触发的修改),而loadStuff1的执行线程可能是主线程也可能是后台线程(取决于调用方)。如果loadStuff1在后台线程执行时读取param,同时主线程正在修改param,就会出现竞态条件,导致读取到不一致的值。
正确做法:
确保读取param的操作在主线程执行,和修改它的线程一致。可以用两种方式:- 给
param添加@MainActor标记,强制所有对param的访问都在MainActor上:@MainActor var param: Param = .a - 在
loadStuff1中显式切换到MainActor读取param:func loadStuff1() async -> Stuff { let currentParam = await MainActor.value { param } await stuffLoader.load(param: currentParam) }
- 给
2. loadStuff2在主线程执行await是否安全?会不会阻塞主线程?
- 安全且不会阻塞主线程:
@MainActor标记让方法始终在主线程执行,但await会暂停当前任务并释放主线程,让主线程可以继续处理UI渲染、用户交互等工作。当stuffLoader.load的异步操作完成后,系统会自动在主线程恢复任务的执行。只要load方法内部没有同步阻塞的代码(比如Thread.sleep),就完全不会导致UI卡顿。
3. loadStuff3中的Task访问param是否会引发竞态条件?
会引发竞态条件:
Task默认在后台的全局Actor执行,而param的修改通常在主线程,后台线程直接读取param时,可能和主线程的修改操作同时发生,导致数据不一致。修复方法:
同样需要在MainActor上读取param,修改后的代码如下:func loadStuff3(completion: @escaping (Stuff) -> Void) { Task { let currentParam = await MainActor.value { self.param } let stuff = await stuffLoader.load(param: currentParam) completion(stuff) } }
内容的提问来源于stack exchange,提问作者Joakim
相关产品推荐
相关产品推荐

