我正在努力理解async let
错误处理,但这在我的脑海中没有多大意义。似乎如果我有两个并行请求,第一个抛出异常的请求不会取消另一个请求。事实上,这只是取决于它们的制造顺序。
我的测试设置:
struct Person {}
struct Animal {}
enum ApiError: Error { case person, animal }
class Requester {
init() {}
func getPeople(waitingFor waitTime: UInt64, throwError: Bool) async throws -> [Person] {
try await waitFor(waitTime)
if throwError { throw ApiError.person }
return []
}
func getAnimals(waitingFor waitTime: UInt64, throwError: Bool) async throws -> [Animal] {
try await waitFor(waitTime)
if throwError { throw ApiError.animal }
return []
}
func waitFor(_ seconds: UInt64) async throws {
do {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * seconds)
} catch {
print("Error waiting", error)
throw error
}
}
}
练习。
class ViewController: UIViewController {
let requester = Requester()
override func viewDidLoad() {
super.viewDidLoad()
Task {
async let animals = self.requester.getAnimals(waitingFor: 1, throwError: true)
async let people = self.requester.getPeople(waitingFor: 2, throwError: true)
let start = Date()
do {
// let (_, _) = try await (people, animals)
let (_, _) = try await (animals, people)
print("No error")
} catch {
print("error: ", error)
}
print(Date().timeIntervalSince(start))
}
}
}
为了简单起见,从现在开始,我将跳过相关的代码行和输出行。
场景1:
async let animals = self.requester.getAnimals(waitingFor: 1, throwError: true)
async let people = self.requester.getPeople(waitingFor: 2, throwError: true)
let (_, _) = try await (animals, people)
结果:
错误:动物1.103397011756897等待CancellationError()时出错
这一个工作正常。较慢的请求需要2秒,但在1秒后(最快的一个投掷时)被取消
场景2:
async let animals = self.requester.getAnimals(waitingFor: 2, throwError: true)
async let people = self.requester.getPeople(waitingFor: 1, throwError: true)
let (_, _) = try await (animals, people)
结果:
错误:动物2014年2月50061798096
现在这不是我所期望的。人们的请求需要1秒才能抛出错误,我们仍然等待2秒,错误是动物性的。我的期望是,这应该是1秒,人们会犯错。
场景3:
async let animals = self.requester.getAnimals(waitingFor: 2, throwError: true)
async let people = self.requester.getPeople(waitingFor: 1, throwError: true)
let (_, _) = try await (people, animals)
结果:
错误:人1.0017549991607666等待CancellationError()时出错
现在这是意料之中的事。这里的区别在于,我交换了请求的顺序,但改为try await (people, animals)
。
哪种方法先抛出并不重要,我们总是会得到第一个错误,所花费的时间也取决于顺序。
这种行为是预期的/正常的吗?我是看到了什么错误,还是测试错误
我很惊讶,人们并没有更多地谈论这件事。我只是在开发者论坛上发现了另一个类似的问题。
请帮忙。:)
来源https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md
async let (l, r) = {
return await (left(), right())
// ->
// return (await left(), await right())
}
意味着异步let的整个初始化器是一个单独的任务,如果在其中进行了多个异步函数调用一个接一个地执行。
这里有一个更结构化的方法,它具有合理的行为。
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
.task {
let requester = Requester()
let start = Date()
await withThrowingTaskGroup(of: Void.self) { group in
let animalTask = Task {
try await requester.getAnimals(waitingFor: 1, throwError: true)
}
group.addTask { animalTask }
group.addTask {
try await requester.getPeople(waitingFor: 2, throwError: true)
}
do {
for try await _ in group {
}
group.cancelAll()
} catch ApiError.animal {
group.cancelAll()
print("animal threw")
} catch ApiError.person {
group.cancelAll()
print("person threw")
} catch {
print("someone else")
}
}
print(Date().timeIntervalSince(start))
}
}
}
这个想法是将每个任务添加到一个投掷组中,然后循环完成每个任务。
科拉一针见血(+1)。元组的async let
将按顺序等待它们。相反,考虑一个任务组。
但您不需要取消组中的其他项目。参见withThrowingTaskGroup(of:returning:body:)
文档中的"任务组取消"讨论:
在任务组的一个任务中引发错误不会立即取消该组中的其他任务。但是,如果你打电话
next()
并传播其错误,所有其他任务已取消。例如,在下面的代码中,没有任何内容被取消组没有抛出错误:withThrowingTaskGroup { group in group.addTask { throw SomeError() } }
相反,此示例抛出
SomeError
并取消组中的所有任务:withThrowingTaskGroup { group in group.addTask { throw SomeError() } try group.next() }
单个任务在对
Group.next()
的相应调用中抛出其错误,这使您有机会处理单个错误或让组重新抛出错误。
或者您可以waitForAll
,这将取消其他任务:
let start = Date()
do {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { let _ = try await self.requester.getAnimals(waitingFor: 1, throwError: true) }
group.addTask { let _ = try await self.requester.getPeople(waitingFor: 2, throwError: true) }
try await group.waitForAll()
}
} catch {
print("error: ", error)
}
print(Date().timeIntervalSince(start))
最重要的是,任务组不会规定等待任务的顺序。(它们也没有规定完成的顺序,所以你经常必须将任务组结果整理成一个独立于顺序的结构,或者重新排序结果。)
您询问如何收集结果。有几个选项:
您可以定义组任务,这样它们就不会"返回"任何东西(即
Void.self
的子级),而是在addTask
调用中更新一个actor(Creatures
,如下),然后从中提取您的元组:class ViewModel1 { let requester = Requester() func fetch() async throws -> ([Animal], [Person]) { let results = Creatures() try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await results.update(with: self.requester.getAnimals(waitingFor: animalsDuration, throwError: shouldThrowError)) } group.addTask { try await results.update(with: self.requester.getPeople(waitingFor: peopleDuration, throwError: shouldThrowError)) } try await group.waitForAll() } return await (results.animals, results.people) } } private extension ViewModel1 { /// Creatures /// /// A private actor used for gathering results actor Creatures { var animals: [Animal] = [] var people: [Person] = [] func update(with animals: [Animal]) { self.animals = animals } func update(with people: [Person]) { self.people = people } } }
您可以定义返回具有关联值的枚举事例的组任务,然后在完成后提取结果:
class ViewModel2 { let requester = Requester() func fetch() async throws -> ([Animal], [Person]) { try await withThrowingTaskGroup(of: Creatures.self) { group in group.addTask { try await .animals(self.requester.getAnimals(waitingFor: animalsDuration, throwError: shouldThrowError)) } group.addTask { try await .people(self.requester.getPeople(waitingFor: peopleDuration, throwError: shouldThrowError)) } return try await group.reduce(into: ([], [])) { previousResult, creatures in switch creatures { case .animals(let values): previousResult.0 = values case .people(let values): previousResult.1 = values } } } } } private extension ViewModel2 { /// Creatures /// /// A private enumeration with associated types for the types of results enum Creatures { case animals([Animal]) case people([Person]) } }
为了完整起见,如果您不想使用任务组,就不必使用任务组。例如,如果先前的任务已取消,则可以手动取消先前的任务。
class ViewModel3 { let requester = Requester() func fetch() async throws -> ([Animal], [Person]) { let animalsTask = Task { try await self.requester.getAnimals(waitingFor: animalsDuration, throwError: shouldThrowError) } let peopleTask = Task { do { return try await self.requester.getPeople(waitingFor: peopleDuration, throwError: shouldThrowError) } catch { animalsTask.cancel() throw error } } return try await (animalsTask.value, peopleTask.value) } }
这不是一个非常可扩展的模式,这就是为什么任务组可能是一个更具吸引力的选择,因为它们为您处理挂起任务的取消(假设您在构建结果时遍历组)。
FWIW,还有其他任务组的替代方案,但在你的问题中没有足够的内容来过于具体地说明这一点。例如,如果所有任务都返回了一个符合Creature
协议的对象数组,那么我可以将一些协议想象为类型实现。
但希望以上说明了一些使用任务组来享受取消功能的模式,同时仍在整理结果。