Swift 抛出的"async let"错误取决于任务的执行顺序



我正在努力理解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))

最重要的是,任务组不会规定等待任务的顺序。(它们也没有规定完成的顺序,所以你经常必须将任务组结果整理成一个独立于顺序的结构,或者重新排序结果。)


您询问如何收集结果。有几个选项:

  1. 您可以定义组任务,这样它们就不会"返回"任何东西(即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
    }
    }
    }
    
  2. 您可以定义返回具有关联值的枚举事例的组任务,然后在完成后提取结果:

    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])
    }
    }
    
  3. 为了完整起见,如果您不想使用任务组,就不必使用任务组。例如,如果先前的任务已取消,则可以手动取消先前的任务。

    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协议的对象数组,那么我可以将一些协议想象为类型实现。

但希望以上说明了一些使用任务组来享受取消功能的模式,同时仍在整理结果。

相关内容

  • 没有找到相关文章

最新更新