我曾经支持无结构任务的方法,每个任务都会等待前一个任务完成。回顾起来,这种方式对我来说有些脆弱。逐渐地(要感谢Rob Napier推动我走向这个方向),我现在使用异步序列,具体来说是苹果的AsyncChannel
库,这更加健壮,并且与现代Swift并发中的异步序列更加一致。
在我们来看你的例子之前,先考虑一下这个串行下载器,其中一个过程(用户点击按钮)将URL
对象发送给另一个过程,后者在一个for
-await
-in
循环中监控通道中的URL:
struct DownloadView: View {
@StateObject var viewModel = DownloadViewModel()
var body: some View {
VStack {
Button("1") { Task { await viewModel.appendDownload(1) } }
Button("2") { Task { await viewModel.appendDownload(2) } }
Button("3") { Task { await viewModel.appendDownload(3) } }
}
.task {
await viewModel.monitorDownloadRequests()
}
}
}
@MainActor
class DownloadViewModel: ObservableObject {
private let session: URLSession = …
private let baseUrl: URL = …
private let folder: URL = …
private let channel = AsyncChannel<URL>()
func monitorDownloadRequests() async {
for await url in channel {
await download(url)
}
}
func appendDownload(_ index: Int) async {
let url = baseUrl.appending(component: "\(index).jpg")
await channel.send(url)
}
func download(_ url: URL) async {
do {
let (location, _) = try await session.download(from: url)
let fileUrl = folder.appending(component: url.lastPathComponent)
try? FileManager.default.removeItem(at: fileUrl)
try FileManager.default.moveItem(at: location, to: fileUrl)
} catch {
print(error)
}
}
}
我们开始 monitorDownloadRequests
,然后将下载请求 append
到通道中。
这会按顺序执行请求(因为 monitorDownloadRequests
有一个 for
-await
循环)。例如,在 Instruments 的“Points of Interest”工具中,我添加了一些 Ⓢ 标志,显示了请求发生的时间间隔,您可以看到这三个请求是按顺序发生的。
![enter image description here](https://istack.dev59.com/bhrRs.webp)
但是通道的奇妙之处在于它们提供了串行行为,而不会引入非结构化并发的问题。它们还可以自动处理取消(如果您需要这种行为)。如果您取消for
-await
-in
循环(在SwiftUI中,当视图被解除时,.task {…}
视图修饰符会自动为我们执行此操作),如果您有一堆非结构化并发,其中一个Task
等待前一个任务,那么处理取消会很快变得混乱。
现在,对于您的情况,您正在询问一个更通用的队列,可以等待任务。那么,您可以拥有一个闭包的
AsyncChannel
:
typealias AsyncClosure = () async -> Void
let channel = AsyncChannel<AsyncClosure>()
E.g.:
typealias AsyncClosure = () async -> Void
struct ExperimentView: View {
@StateObject var viewModel = ExperimentViewModel()
var body: some View {
VStack {
Button("Red") { Task { await viewModel.addRed() } }
Button("Green") { Task { await viewModel.addGreen() } }
Button("Blue") { Task { await viewModel.addBlue() } }
}
.task {
await viewModel.monitorChannel()
}
}
}
@MainActor
class ExperimentViewModel: ObservableObject {
let channel = AsyncChannel<AsyncClosure>()
func monitorChannel() async {
for await task in channel {
await task()
}
}
func addRed() async {
await channel.send { await self.red() }
}
func addGreen() async {
await channel.send { await self.green() }
}
func addBlue() async {
await channel.send { await self.blue() }
}
func red() async { … }
func green() async { … }
func blue() async { … }
}
这将产生:
![enter image description here](https://istack.dev59.com/Bpzaf.webp)
在这里,我再次使用Instruments来可视化正在发生的事情。我快速地连续点击了“红色”、“绿色”和“蓝色”按钮两次。然后,我观察了这三个三秒任务的六个相应时间间隔。接着,我第二次重复了这个六次点击的过程,但这一次在它们完成之前取消了相关视图,中途取消了第二组按钮点击的绿色任务,展示了AsyncChannel
(以及异步序列)无缝取消的能力。
现在,希望你原谅我,因为我省略了创建所有这些“兴趣点”标志和时间间隔的代码,因为它增加了很多不相关的内容,与我们所关心的问题无关(但如果你感兴趣,可以参考this)。但是,希望这些可视化能够帮助说明正在发生的事情。
最重要的信息是,AsyncChannel
(以及它的姊妹AsyncThrowingChannel
)是保持结构化并发性的好方法,但同时也能获得串行(或受限制的行为,如我们在answer的结尾所展示的),这是我们以前通过队列获得的,但是使用异步任务。
我必须承认,尽管这个AsyncClosure
示例有望回答你的问题,但在我看来有点勉强。 我现在已经使用AsyncChannel
几个月了,并且个人始终有一个更具体的对象由通道处理(例如URL、GPS位置、图像标识符等)。 这个闭包示例似乎试图过于努力地重现老式的调度/操作队列行为。
await
并且其他任务可能会在同一队列上被调度。 - Rob Napier