Swift Actor方法能否被并发交互?WWDC示例引发的困惑
我正在观看WWDC2021的《用Swift Actor保护可变状态》视频,里面有个示例展示了Actor方法被并发调用的问题:
假设两个并发任务同时尝试获取同一张图片。第一个任务发现缓存中没有对应条目,便开始从服务器下载图片,随后因下载耗时较长被挂起。在第一个任务下载期间,服务器上该URL对应的图片可能已经更新。此时第二个并发任务尝试获取该图片,由于第一个任务的下载尚未完成,缓存里还是没有条目,于是也启动了下载,同样在下载过程中被挂起。一段时间后,第一个任务下载完成,回到Actor中填充缓存并返回一张猫的图片;接着第二个任务下载完成,它会用自己获取到的悲伤猫图片覆盖缓存里的同一条目。结果就是,同一个URL对应的图片被替换了,即便缓存已经被填充过。

Actor的核心设计不就是保证同一时间只有一个调用者能直接和它交互吗?
我写了一段示例代码,运行后发现“BEGIN increment”总是紧跟“END increment”,后续对increment方法的调用必须等待前一个完成:
actor Counter { var count = 1 func increment() { print("BEGIN increment") let url = URL(string: "https://google.com")! let data = try! Data(contentsOf: url) let string = String(data: data, encoding: .utf8) ?? "" print("END increment") count += 1 } } struct ContentView: View { @State var counter = Counter() var body: some View { Button { Task.detached { await counter.increment() } } label: { Text("Click") } } }
问题解析
你遇到的差异核心在于同步阻塞操作和异步挂起操作的区别:
你的代码里,
Data(contentsOf:)是同步阻塞调用,它会一直占用Actor的执行线程,直到数据加载完成。在这个过程中,Actor的执行权没有被释放,后续任务必须排队等待当前方法完全执行完毕才能进入Actor,所以你看到的是“BEGIN”和“END”连续出现,没有并发情况。而WWDC视频里的下载逻辑是异步的(比如使用
URLSession的异步API),当Actor方法执行到await下载任务时,当前任务会挂起并释放Actor的执行权。这时候,等待中的第二个任务就可以进入Actor执行方法的前半部分——检查缓存。由于第一个任务还在下载,缓存还没被填充,第二个任务就会发起自己的下载请求。
Actor的串行执行规则是:在Actor的执行上下文中,代码是串行执行的,但遇到await时会让出执行权。这就导致了视频里的场景:两个任务都能进入Actor的方法执行缓存检查逻辑,因为第一个任务在异步下载时让出了Actor的使用权。
要模拟视频里的场景,你需要把同步的Data(contentsOf:)改成异步的网络请求,比如:
actor ImageCache { private var cache: [URL: Data] = [:] func fetchImage(url: URL) async throws -> Data { if let cachedData = cache[url] { return cachedData } // 异步网络请求,会触发挂起并释放Actor执行权 let (data, _) = try await URLSession.shared.data(from: url) cache[url] = data return data } }
当两个任务同时调用fetchImage时,第一个任务执行到await URLSession.shared.data(from:)就会挂起,Actor的执行权被释放,第二个任务进入方法检查缓存——此时缓存还是空的,所以它也会发起异步下载。等两个下载任务先后完成后,就会出现第二个任务覆盖缓存的情况。
内容的提问来源于stack exchange,提问作者onmyway133

