TL;有关该问题的代码示例,请参见下面的 DR。
我有两个函数async update()
更新服务器上的一些数据并等待响应。我可以选择提供回调或使函数异步并等待响应。第二个函数fetch()
位于不同的类中(实际上是不同的@StateObject,即 ObservableObject 类 - 我使用的是 SwiftUI)。
函数fetch()
是从视图中触发的,但它应该"等待"update()
函数完成(如果有一个正在运行)再执行。
我想将两个类实例(这些函数所属)分开。因此,我不能显式使用回调或类似内容。在搜索和发现同步/异步、串行/并发DispatchQueue
、DispatchGroup
、Task
、OperationQueue
和概念后,我对使用什么感到非常困惑,因为它们中的许多似乎都是非常相似的工具。
-
DispatchQueue(&Task)
起初,我认为 DispatchQueue 是要走的路,因为文档似乎准确地描述了我需要的东西:">......调度队列以串行或并发方式执行任务。...">DispatchQueue.main 是串行的,所以我认为简单地执行这样的两个函数调用会等待
update()
完成,如果我的应用程序中的任何位置启动了一个函数调用,之后fetch()
会运行。但显然:DispatchQueue.main.async { self.example = await self.update() }
这将引发错误:
无法将类型"() 异步 -> ()"的函数传递给需要同步函数类型的参数
这对我来说已经很奇怪了,因为它的名字
DispatchQueue.main.
async
.我可以通过将调用包装在Task {}
中来避免这种情况,但我想它会在不同的线程上运行,因为它在运行fetch()
函数之前没有完成Task
。因此,它不再是串行的:DispatchQueue.main.async { Task { print("1") try! await Task.sleep(nanoseconds: 10_000_000_000) print("2") } Task { print("3") } } // prints 1,3, ..., 2
根据这个,使用
DispatchQueue.main.sync
似乎更符合我需要的东西,但我得到错误:调用实例方法"sync"时没有完全匹配
项有没有办法使用
DispatchQueue
来实现我的目标?(我还尝试创建自己的队列,结果与使用全局主队列相同。我想使用异步函数并阻止此队列上的任何其他函数,直到完成执行) -
调度组
接下来,我尝试使用此处所示的
DispatchGroup
,但由于必须传递此DispatchGroup()
实例以在两个类中引用,因此已经放弃了。这是实现我的目标的唯一途径吗?我可以避免在初始时将一个对象传递给两个类吗? -
操作队列
进一步阅读后,我偶然发现了
OperationQueue
这里。同样,这似乎解决了我的问题,但我必须再次将此OperationQueue
对象传递给两个类。
有人可以解释这些方法与我的问题相关的区别吗?哪一个是"正确"的方法?我认为不必传递某些对象更容易,但是如何使用全局DispatchQueue
来串行执行一些异步函数呢?
这里的 MRE 在单击"开始长任务"并立即获取控制台后应显示为:"1","2","这些将是一些结果">
import SwiftUI
class Example1ManagerState: ObservableObject {
func update() async {
print("1")
try! await Task.sleep(nanoseconds: 10_000_000_000)
print("2")
}
}
class Example2ManagerState: ObservableObject {
func fetch() {
print("these would be some results")
}
}
struct ContentView2: View {
@StateObject var ex1 = Example1ManagerState()
@StateObject var ex2 = Example2ManagerState()
var body: some View {
Button {
Task {
await ex1.update()
}
} label: {
Text("start long task")
}
Button {
ex2.fetch()
} label: {
Text("fetch")
}
}
}
根据您的示例,您只需要添加一行
Button {
Task {
await ex1.update()
ex2.fetch() // here this line doesn’t run unless update is done
}
} label: {
Text("start long task")
}
Swift 并发非常适合处理本身异步任务之间的依赖关系。我们可以将第一个async
函数调用包装在一个Task
中,然后await
该Task
的结果:
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View{
VStack{
Text(viewModel.results)
Button("Example 1 - update") { viewModel.update() }
Button("Example 2 - fetch") { viewModel.fetch() }
}
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var results = ""
var task: Task<Void, Never>?
let example1 = Example1ManagerState()
let example2 = Example2ManagerState()
func update() {
task = Task {
results = "starting update"
let updateResults = await example1.update()
results = updateResults
}
}
func fetch() {
Task {
_ = await task?.result
results = "starting fetch"
let fetchResults = await example2.fetch()
results = fetchResults
}
}
}
actor Example1ManagerState {
func update() async -> String {
let seconds: TimeInterval = .random(in: 2...3)
try? await Task.sleep(for: .seconds(seconds))
return "(#function) finished (seconds)"
}
}
actor Example2ManagerState {
func fetch() async -> String {
let seconds: TimeInterval = .random(in: 2...3)
try? await Task.sleep(for: .seconds(seconds))
return "(#function) finished (seconds)"
}
}
上面,我已经将业务逻辑从"视图"中抽象出来,并抽象为"视图模型"(目标是更好地分离职责,从而简化单元测试等),但这与更广泛的观点无关。观察结果是,无论谁调用update
和fetch
,都应该管理这些任务之间的依赖关系(在这种情况下,视图模型具有Task
属性),并且此逻辑不会合并到Example1ManagerState
或Example2ManagerState
中。
其他一些随机观察结果:
我们通常应该避免将 GCD API 与 Swift 并发的
async
-await
混合在一起。我建议不要在 Swift 并发代码中使用DispatchQueue.main.async {…}
。一般来说,这是一种不好的做法,在某些情况下,它可能会导致严重的问题。编译器越来越好地警告我们这种误用,并且很可能是 Swift 6 中的一个硬错误。如果您没有使用 Swift 并发性,并且正在考虑传统方法,请对问题中的要点进行一些观察:
DispatchQueue
非常适合管理添加到队列的代码块。你可以同步(sync
)或异步(async
)调度这些代码块。该同步/异步调度指示在调度块运行时是否阻止调用方的线程。但是
DispatchQueue
只有在调度块中的代码本身同步运行时才有用。不要将调用方的同步/异步性质(即调用方是否等待)与调度块内代码的同步/异步性质混为一谈。例如,假设您将
URLSession
dataTask
分派到调度队列。该队列将管理这些URLSession
请求的创建,但不管理等待这些异步请求实际完成。(哈哈。通常,请避免使用调度队列来启动本身异步的任务。
为了在遗留代码库中解决此问题,我们将转向操作队列来管理本身异步任务之间的依赖关系。它非常出色地管理了异步操作之间的依赖关系。(如果实现得当,它还具有出色的职责分离功能,为我们提供了高度内聚、松散耦合的代码。
但是编写异步
Operation
子类是非常繁琐的。一旦你设置了它,它就是一个很好的模式,但是Operation
对象的自定义异步子类的正确实现远非直观或明显。例如,https://stackoverflow.com/a/48104095/1271826。快速并发,
async
-await
,消除了所有这些丑陋。你提到
DispatchGroup
.这实际上是为了在多个 GCD 工作项之间建立依赖关系。(例如,你可能希望在一堆其他分派块之后触发代码块,并行运行,完成。因为您可以手动
enter
和leave
调度组,所以我们可以扭曲自己来使用它来管理代码块之间的依赖关系,这些代码块本身是异步的,但同样,Swift 并发处理这一点要优雅得多。
关于您不想传递管理依赖项的遗留对象(即操作队列或调度组或其他任何内容)的问题,您绝对正确:您希望避免这种情况。
例如,在操作队列环境中,您不应将操作队列传递给此其他管理器对象,而是重构代码,以便管理器对象可以执行操作,并让调用方将其添加到自己的操作队列中。这样可以避免传递队列。
调度组也是如此。与其传递
DispatchGroup
对象(此时诊断问题、理解依赖关系等变得非常困难),不如为两个ManagerState
方法提供一个完成处理程序,调用方将在调用方法之前enter
组,并在完成处理程序中leave
。底线,是的,避免传递操作队列或调度组或你周围的东西。就像我在调用代码(在我的示例中为视图模型)中隔离了
Task
依赖项一样,您将在传统类型的模式中执行相同的操作,完全避免这些管理器类型中的依赖项纠缠,并让调用方处理这个问题。
简而言之,Swift 并发应该满足您的需求,并让您摆脱这些脆弱而复杂的 GCD 实现的复杂性。
您可以考虑几个不同的选项来实现让 fetch 函数等待更新函数完成然后再执行的目标。以下是一些选项及其差异:
DispatchQueue:DispatchQueue是一种串行(一次一个)或并发(同时)执行任务的方法。可以使用串行调度队列来确保任务按照添加到队列中的顺序一次执行一个任务。要使用串行调度队列,您可以创建自己的调度队列并将属性 .isSerial 设置为 true。或者,您可以使用 .main 队列,它是用于在主线程上执行任务的串行队列。但是,您不能将 await 关键字与 DispatchQueue 一起使用,因为它不知道 async/await。相反,您可以使用闭包或函数作为要在队列上执行的任务。
调度组:调度组允许您将多个任务分组在一起,并等待所有任务完成,然后再继续下一组任务。您可以使用调度组来确保提取函数在执行之前等待更新函数完成。为此,您可以在执行之前将更新函数添加到调度组,然后在获取函数中调用 DispatchGroup.wait() 以等待更新函数完成,然后再继续。您需要将对 DispatchGroup 对象的引用传递给这两个类才能使用它。