执行多个并发(async let)任务并仅在所有任务都失败时抛出错误

问题描述 投票:0回答:2

例如,我需要从不同来源获取两个相同类型的数组。这两个请求都可能会失败,但是当这些请求同时失败时,我需要显示任何错误(由服务器给出)。

这是一个常见的示例,但当任何请求失败时它都会抛出错误:

func test() async throws -> [Any] {
    async let t1 = getArr()
    async let t2 = getArr2()
    return try await t1 + t2
}

func getArr() async throws -> [Any] {
    []
}

func getArr2() async throws -> [Any] {
    []
}

同时,如果我使用

try?
,我会丢失请求返回的错误。

我也可以删除

throws
并用
Result<[Any], Error>
替换返回结果,但看起来很奇怪。

如何正确解决这个问题?

swift async-await concurrency task throws
2个回答
2
投票

你绝对应该在这里使用

Result
。为了方便起见,首先编写一个
Result
初始化程序来捕获异步抛出闭包中的错误。

extension Result {
    init(asyncCatching block: () async throws -> Success) async where Failure == Error {
        do {
            self = .success(try await block())
        } catch {
            self = .failure(error)
        }
    }
}

假设您希望

test
在所有
async let
抛出时抛出所有错误,请将
[Error]
也设为
Error
类型。

extension Array: Error where Element: Error {}

那么就很简单:

func test() async throws -> [Any] {
    async let t1 = Result { try await getArr() }
    async let t2 = Result { try await getArr2() }
    switch (await t1, await t2) {
    case let (.failure(e1), .failure(e2)):
        return [e1, e2]
    case let (.success(s), .failure), let (.failure, .success(s)):
        return s
    case let (.success(s1), .success(s2)):
        return s1 + s2
    }
}

对于“将

Result
的集合减少为单个值或抛出”操作的更通用版本,您可以编写:

func accumulateResults<T, U, Failure: Error>(_ results: [Result<T, Failure>], identity: U, reduce: (U, T) -> U) throws -> U {
    var ret = identity
    var success = false
    var failures = [Failure]()
    for result in results {
        switch result {
        case .success(let s):
            ret = reduce(ret, s)
        case .failure(let e):
            if success { break }
            failures.append(e)
        }
    }
    return if success { ret } else { throw failures }
}

如果不使用

Result
(或重新发明类似的东西),
test
只能抛出最后一个错误,因为你无法存储任何以前的错误(
Result
的确切工作)。

func testWithoutResult() async throws -> [Any] {
    async let t1 = getArr()
    async let t2 = getArr2()
    var result = [Any]()
    var success = false
    do {
        result.append(try await t1)
        success = true
    } catch {}
    do {
        result.append(try await t2)
    } catch {
        if !success { throw error }
    }
    return result
}

对于每个非最终异步操作,编写

do
块并设置
success = true
。对于最终的异步操作,请改为在 catch 块中
if !success { throw error }


0
投票

虽然 Swift 并发性最大限度地减少了我们对

Result
类型的依赖(有自己的、更自然的抛出错误和返回成功的方式),但
Result
类型仍然受到非常多的支持。

例如,

Task
有自己的
result
属性。因此,在这种情况下我可能会利用它并返回成功/失败值的数组:

func test() async -> [Result<[Int], Error>] {
    let t1 = Task { try await getArr() }
    let t2 = Task { try await getArr2() }
    
    return await withTaskCancellationHandler { 
        await [t1.result, t2.result]
    } onCancel: { 
        t1.cancel()
        t2.cancel()
    }
}

func getArr() async throws -> [Int] {
    if Bool.random() {
        return [1, 2, 3]
    } else {
        throw RandomChanceError.badLuck
    }
}

func getArr2() async throws -> [Int] {
    if Bool.random() {
        return [4, 5, 6]
    } else {
        throw RandomChanceError.badLuck
    }
}

enum RandomChanceError: Error {
    case badLuck
}

非结构化并发的唯一技巧是您有责任添加取消处理,如上面使用

withTaskCancellationHandler
所示。 (您当前可能不会预期取消此
test
方法,但在编写非结构化并发时始终添加取消处理仍然是谨慎的做法。)

无论如何,调用者可以迭代这些结果:


for result in await test() {
    switch result {
    case .success(let value): print(value)
    case .failure(let error): print(error)
    }
}

在您的示例中,您将返回

[Any]
getArr
getArr2
真的每个都返回异构数组吗?或者您使用
[Any]
因为这两个函数实际上只是返回两个不同类型的不同同构数组?

如果后者是真的,我可能建议退休

[Any]
,因为这实际上并不能反映两个独立同质数组的性质。例如,在下面,我考虑了一个场景,其中第一个(我已重命名为
getNumbers
)返回一种类型的同构数组,第二个(我已重命名为
getStrings
)返回一个同构数组,但类型不同:

func test() async throws {
    let numbersAndStrings = try await getNumbersAndStrings()

    switch numbersAndStrings.numbers {
    case .success(let numbers): print(numbers)
    case .failure(let error):   print(error)
    }

    switch numbersAndStrings.strings {
    case .success(let strings): print(strings)
    case .failure(let error):   print(error)
    }
}

func getNumbersAndStrings() async throws -> (numbers: Result<[Int], Error>, strings: Result<[String], Error>) {
    let t1 = Task { try await getNumbers() }
    let t2 = Task { try await getStrings() }

    return try await withTaskCancellationHandler {
        let results = await (
            numbers: t1.result,
            strings: t2.result
        )
        try Task.checkCancellation()
        return results
    } onCancel: {
        t1.cancel()
        t2.cancel()
    }
}

func getNumbers() async throws -> [Int] {
    try await Task.sleep(for: .seconds(.random(in: 1...3)))
    if Bool.random() {
        return [1, 2, 3]
    } else {
        throw SomeError.badLuck
    }
}

func getStrings() async throws -> [String] {
    try await Task.sleep(for: .seconds(.random(in: 1...3)))
    if Bool.random() {
        return ["four", "five", "six"]
    } else {
        throw SomeError.badLuck
    }
}

enum SomeError: Error {
    case badLuck
}
© www.soinside.com 2019 - 2024. All rights reserved.