Swift中Await使用主线程还是协作线程池?实测疑问求解
问题背景
我清楚await不会直接创建新线程,只是挂起当前函数让线程去处理其他任务,但不确定系统会选择哪个线程继续执行被await的任务。在Xcode 14里测试了三个fetch方法,结果如下:
ViewModel.fetch1:运行在主线程ViewModel.fetch2:运行在协作线程池ContentView.fetch:运行在主线程
测试代码:
class ViewModel: ObservableObject { @Published var string = "" func fetch1() { let url = URL(string: "https://google.com")! let data = try! Data(contentsOf: url) self.string = String(data: data, encoding: .utf8) ?? "" } func fetch2() async { let url = URL(string: "https://google.com")! let data = try! Data(contentsOf: url) self.string = String(data: data, encoding: .utf8) ?? "" } } struct ContentView: View { @State var string = "" @StateObject var vm = ViewModel() var body: some View { VStack { Button { Task { await vm.fetch1() } Task { await vm.fetch2() } Task { await fetch() } } label: { Text("Fetch") } Text(string) Text(vm.string) } } private func fetch() async { let url = URL(string: "https://google.com")! let data = try! Data(contentsOf: url) self.string = String(data: data, encoding: .utf8) ?? "" } }
我知道Swift的async/await模型不用过度纠结线程,但必须避免在主线程执行繁重任务。我能理解ContentView.fetch和ViewModel.fetch1跑主线程是因为SwiftUI body里创建的非结构化Task继承了Main Actor上下文,但为什么标记成async的ViewModel.fetch2会被调度到协作线程池,而不是继承主线程上下文?
问题解析
1. 主线程执行的两个方法:继承了Main Actor上下文
按钮的action闭包本身就在**主线程(Main Actor)**执行,闭包里创建的非结构化Task会自动继承当前的Actor上下文:
ViewModel.fetch1是同步函数,调用它的Task在主线程,所以它直接在主线程运行,没有线程切换。ContentView.fetch是结构体里的async函数,Swift结构体默认不绑定特定Actor,但调用它的Task处于Main Actor上下文,所以这个async函数会继承该上下文,在主线程执行。
2. ViewModel.fetch2跑协作线程池:无Actor绑定的异步函数调度规则
你的ViewModel虽然遵循ObservableObject,但没加@MainActor标记。在SwiftUI里,只有标记了@MainActor的ObservableObject,它的所有属性和方法才会默认绑定到Main Actor。
当从Main Actor上下文调用一个没有绑定到任何Actor的async函数时,Swift并发系统会自动把这个函数调度到全局协作线程池——因为系统判断这个函数没有必须在主线程执行的约束,选择后台线程能避免阻塞主线程,符合并发模型的优化逻辑。
补充:虽然
fetch2里修改了@Published属性,但因为ViewModel没绑定Main Actor,这个修改本身是在后台线程做的。不过SwiftUI的@Published会自动把值更新投递到主线程,所以UI还是能正常刷新。
如何确保繁重任务不跑主线程?
方案1:给ViewModel绑定Main Actor,手动切换后台执行任务
如果你的ViewModel需要处理UI状态(大部分场景都是如此),先给它加@MainActor标记,然后用Task.detached把繁重任务切到后台,完成后再回主线程更新UI:
@MainActor class ViewModel: ObservableObject { @Published var string = "" func fetch2() async { // 脱离Main Actor,在协作线程池执行耗时操作 let data = await Task.detached(priority: .background) { let url = URL(string: "https://google.com")! return try! Data(contentsOf: url) }.value // 已经在Main Actor上下文,直接更新UI self.string = String(data: data, encoding: .utf8) ?? "" } }
方案2:用异步网络API替代同步API
Data(contentsOf:)是同步阻塞的API,哪怕在async函数里用也会阻塞当前线程。推荐用URLSession的异步API,它本身就会在后台执行,不用手动切换线程:
func fetch2() async { let url = URL(string: "https://google.com")! let (data, _) = try! await URLSession.shared.data(from: url) self.string = String(data: data, encoding: .utf8) ?? "" }
方案3:让ViewModel不绑定Main Actor
如果ViewModel不需要处理UI逻辑,可以不加@MainActor,这样它的async方法默认会在协作线程池执行。不过更新@Published属性时,复杂场景建议手动切回主线程,避免潜在的线程问题。
内容的提问来源于stack exchange,提问作者onmyway133

