Swift async/await & Task:如何添加进度完成?

Joh*_*Doe 1 concurrency asynchronous async-await swift

我正在学习 Swift 的 async/await,我想编写一个函数,该函数期望多个调用同时发生(而不是顺序),并且每次一个调用完成时,都会获得进度完成。我试过这个:

enum Call: String {
    case first = "first"
    case second = "second"
    case third = "third"
    
    var delay: TimeInterval {
        switch self {
        case .first: return 4.0
        case .second: return 7.0
        case .third: return 2.0
        }
    }
}

func load(progress: @escaping (Double) -> Void) async {
    let calls = [Call.first, .second, .third]
    var tasks: [Task<Void, Never>] = []
    for call in calls {
        tasks.append(Task.detached { [weak self] in
            guard let self else { return }
            await self.testFunc(call)
        })
    }
    var count = 0
    for task in tasks {
        await task.value
        count += 1
        progress(Double(count) / Double(calls.count))
    }
    return
}

func testFunc(_ call: Call) async {
    print("Start call \(call.rawValue) [\(Date())]")
    return await withCheckedContinuation { continuation in
        delay(call.delay) {
            print("End call \(call.rawValue) [\(Date())]")
            continuation.resume()
        }
    }
}

func delay(_ seconds: Double, completion: @escaping () -> Void) {
    let popTime = DispatchTime.now() + Double(Int64(Double(NSEC_PER_SEC) * seconds)) / Double(NSEC_PER_SEC)
    DispatchQueue.main.asyncAfter(deadline: popTime) {
        completion()
    }
}
Run Code Online (Sandbox Code Playgroud)

像这样调用:

print("Go [\(Date())]")
Task {
    await load { progress in
        print("Progress [\(Date())]: \(progress)")
    }
    print("Done!")
}
Run Code Online (Sandbox Code Playgroud)

我希望这段代码同时启动三个调用,并发执行,第三个调用需要 2 秒,应该首先完成,然后第一个调用需要 4 秒,第二个调用需要 7 秒。另外,每次调用完成时,我希望调用进度完成块。

但这里是输出:

Go [2023-05-03 11:10:34 +0000]
Start call first [2023-05-03 11:10:34 +0000]
Start call second [2023-05-03 11:10:34 +0000]
Start call third [2023-05-03 11:10:34 +0000]
End call third [2023-05-03 11:10:36 +0000]
End call first [2023-05-03 11:10:38 +0000]
Progress [2023-05-03 11:10:38 +0000]: 0.3333333333333333
End call second [2023-05-03 11:10:41 +0000]
Progress [2023-05-03 11:10:41 +0000]: 0.6666666666666666
Progress [2023-05-03 11:10:41 +0000]: 1.0
Done!
Run Code Online (Sandbox Code Playgroud)

显然,当我在控制台中期望时,进度线没有被打印。这是为什么?

感谢您的帮助

Rob*_*Rob 5

您正在同时运行任务,但按顺序等待结果。考虑:

\n
for task in tasks {\n    await task.value\n    \xe2\x80\xa6\n}\n
Run Code Online (Sandbox Code Playgroud)\n

即使任务同时运行,上面的任务也会await按顺序依次运行各自的值。await即,在第一个任务产生值之前,它甚至不会到达第二个任务的 。如果您使用\xe2\x80\x9c任务组\xe2\x80\x9d,它将让您await按照它们完成的顺序进行操作。

\n
\n

那么,一些细节:

\n
    \n
  1. 您应该尽可能避免不必要的非结构化并发( 或Task {\xe2\x80\xa6}) 。Task.detached {\xe2\x80\xa6}如果我们保持结构化并发,我们就会享受任务取消的自动处理。

    \n
  2. \n
  3. 不要保留自己的任务数组,而是使用 \xe2\x80\x9ctask group\xe2\x80\x9d (例如withTaskGroupwithThrowingTaskGroup)。然后你可以await分组,任务可以按他们想要的任何顺序完成。

    \n
  4. \n
  5. 请注意,当您使用任务组时,我们现在需要担心计数器的线程安全性。因此,您可以创建一个参与者来跟踪进度:

    \n
    actor CallProgress {\n    var total = 0\n    var count = 0\n    var fractionCompleted: Double { Double(count) / Double(total) }\n\n    func add() {\n        total += 1\n    }\n\n    func finish() {\n        count += 1\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    然后像这样使用它:

    \n
    func load(progressHandler: @Sendable @escaping (Double) -> Void) async {\n    let calls: [Call] = [.first, .second, .third]\n\n    let progress = CallProgress()\n\n    await withTaskGroup(of: Void.self) { group in\n        for call in calls {\n            await progress.add()\n\n            group.addTask { [self, progress] in\n                await testFunc(call)\n                await progress.finish()\n                await progressHandler(progress.fractionCompleted)\n            }\n        }\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    或者你可以使用Progress对象。

    \n
    func load(progressHandler: @Sendable @escaping (Double) -> Void) async {\n    let calls: [Call] = [.first, .second, .third]\n\n    let progress = Progress()\n\n    await withTaskGroup(of: Void.self) { group in\n        for call in calls {\n            progress.totalUnitCount += 1\n\n            group.addTask { [self, progress] in\n                await testFunc(call)\n                progress.completedUnitCount += 1\n                progressHandler(progress.fractionCompleted)\n            }\n        }\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    请注意,Progress提供了一些相当丰富的功能(您可以汇总对象树;它fractionCompleted是可观察的;与 UI 类型集成UIProgressView;等等),但我只是利用包装completedUnitCount和的线程安全对象totalUnitCount

    \n

    还要注意的是,应该关闭Sendable。您可能必须将 \xe2\x80\x9cstrict 并发检查\xe2\x80\x9d 构建设置设置为 \xe2\x80\x9ccomplete\xe2\x80\x9d 才能查看所有这些线程安全问题。

    \n
  6. \n
  7. 或者,我们可以使用 来更新进度,而不是使用闭包AsyncSequence。例如:

    \n
    func loadUpdates() async -> AsyncStream<Double> {\n    let calls: [Call] = [.first, .second, .third]\n\n    let progress = Progress()\n\n    return AsyncStream { continuation in\n        let task = Task {\n            await withTaskGroup(of: Void.self) { group in\n                for call in calls {\n                    progress.totalUnitCount += 1\n                    group.addTask { [self, progress] in\n                        await testFunc(call)\n                        progress.completedUnitCount += 1\n                        continuation.yield(progress.fractionCompleted)\n                    }\n                }\n                await group.waitForAll()\n                continuation.finish()\n            }\n        }\n\n        continuation.onTermination = { _ in\n            task.cancel()\n        }\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    进而:

    \n
    func start() async {\n    print("Go [\\(Date())]")\n\n    for await progress in await loadUpdates() {\n        print("Progress [\\(Date())]: \\(progress)")\n    }\n\n    print("Done!")\n}\n
    Run Code Online (Sandbox Code Playgroud)\n
  8. \n
  9. 还有很多其他模式。例如,在 UIKit 中,我们可能会使用 return aProgress并仅设置observedProgressUIProgressView。或者在 SwiftUI 中,如果这是一个ObservableObject,我们可能会自己更新@Published\xe2\x80\x9cfractioncompleted\xe2\x80\x9d 值。问题中没有足够的上下文来更具体。

    \n
  10. \n
\n

最重要的是,我们可能倾向于选择其他模式而不是闭包,并且我们会使用任务组来跟踪我们的任务。

\n