我正在使用苹果的Vision框架对一张图像运行多个检测请求.

VNImageRequestHandler.perform()调度多个请求(可能在单独的线程中).完成处理程序是为各个请求定义的.最后,结果应该分配给一个类var.

根据我的理解,延续是将基于完成处理程序的API转换为异步代码的最佳方式.但是,续订只能恢复一次.

AsyncStream似乎是一个更可行的解决方案.然而,结果可以以任何顺序到达,而我只有固定数量的请求.

与以下代码一起使用的最常用的异步范例是什么?

// non-async code
let handler = VNImageRequestHandler(url: url)
let faceQualityRequest = VNDetectFaceCaptureQualityRequest { request, err in
    guard err == nil else { return }
    self.faceDetectionResults = request.results
}
let featurePrintRequest = VNGenerateImageFeaturePrintRequest { request, err in
    guard err == nil else { return }
    self.featurePrintResults = request.results
}
try handler.perform([faceQualityRequest, featurePrintRequest])

推荐答案

我理解为什么当看到一系列闭包时,人们可能会想到异步序列或任务组,但我们应该注意到perform是同步的,而不是异步的.因此,我可能会建议相关问题与其说是"我如何处理这些闭包",不如说是"我如何在SWIFT并发中利用一个缓慢的同步API".

以下是一些观察结果:

  1. 鉴于perform是一个同步函数,您应该知道应该避免阻塞当前参与者.因此,从理论上讲,你可以把它转移到一项独立的任务中.

    话虽如此,但即使是这样也不是谨慎的.例如,在WWDC 2022的《S Visualize and optimize Swift concurrency》中,苹果明确建议将阻止代码移出SWIFT并发系统:

    一定要避免阻塞任务中的调用.…如果您有需要执行这些操作的代码,请将该代码移到并发线程池之外--例如,通过在DispatchQueue上运行它--并使用延续将其连接到并发世界.

    async/await: How do I run an async function within a @MainActor class on a background thread?

  2. 您现在关注的是提供给VNDetectFaceCaptureQualityRequestVNGenerateImageFeaturePrintRequest的闭包.但是这些闭包是可选的,您可以使用它们各自的results(herehere).

因此,您可能会得到如下结果:

func faceAndFeature(for url: URL) async throws -> ([VNFaceObservation], [VNFeaturePrintObservation]) {
    try await withCheckedThrowingContinuation { continuation in
        DispatchQueue.global().async {
            let faceQualityRequest = VNDetectFaceCaptureQualityRequest()
            let featurePrintRequest = VNGenerateImageFeaturePrintRequest()

            let handler = VNImageRequestHandler(url: url)

            do {
                try handler.perform([faceQualityRequest, featurePrintRequest])
                continuation.resume(returning: (faceQualityRequest.results ?? [], featurePrintRequest.results ?? []))
            } catch {
                continuation.resume(throwing: error)
            }
        }
    }
}

例如,

let (faceQuality, featurePrint) = try await faceAndFeature(for: url)
print("faceQuality =", faceQuality)
print("featurePrint =", featurePrint)

It should be noted that the above will not return any results until perform finishes all the requests. You can, alternatively, create two AsyncChannel instances, one for faces and one for features. 例如,

let faceChannel = AsyncThrowingChannel<[VNFaceObservation], Error>()
let featureChannel = AsyncThrowingChannel<[VNFeaturePrintObservation], Error>()

func analyzeImage(at url: URL) async throws {
    try await withCheckedThrowingContinuation { continuation in
        DispatchQueue.global().async {
            let faceQualityRequest = VNDetectFaceCaptureQualityRequest { request, error in
                Task { [weak self] in
                    guard let self else { return }

                    guard error == nil, let results = request.results as? [VNFaceObservation] else {
                        faceChannel.fail(error ?? VisionError.invalidRequestType)
                        return
                    }

                    await faceChannel.send(results)
                }
            }

            let featurePrintRequest = VNGenerateImageFeaturePrintRequest { request, error in
                let results = request.results
                Task { [weak self, results] in
                    guard let self else { return }

                    guard error == nil, let results = results as? [VNFeaturePrintObservation] else {
                        featureChannel.fail(error ?? VisionError.invalidRequestType)
                        return
                    }

                    await featureChannel.send(results)
                }
            }

            let handler = VNImageRequestHandler(url: url)

            do {
                try handler.perform([faceQualityRequest, featurePrintRequest])
                continuation.resume()
            } catch {
                continuation.resume(throwing: error)
            }
        }
    }
}

然后监控这些频道:

await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask {
        for try await observations in self.faceChannel {
            …
        }
    }

    group.addTask {
        for try await observations in self.featureChannel {
            …
        }
    }
}

最后,启动请求:

try await analyzeImage(at: url)

以下是几点注意事项:

  1. Vision框架似乎还没有对Sendable进行审核,因此您可能希望将其指定为@preconcurrency,以消除有关这一点的恼人警告:

    @preconcurrency import Vision
    
  2. 请注意,在SwiftUI中,您可以从.task视图修改器启动这些for-await-in循环,当该视图被取消时,它们将被取消.在UIKit/AppKit中,您必须保留对启动它们的Task的引用,并在有问题的视图消失时手动引用cancel个.

Swift相关问答推荐

如何在SwiftUI中创建具有圆角顶部和锯齿状底部边缘的自定义Shape,类似于撕破的纸?

编写Swift字符串的属性包装时遇到问题

SwiftUI-如何使用剩余时间制作倒计时计时器

';NSInternal不一致异常';,原因:';可见导航栏Xcode 15.0 Crash请求布局

使用序列初始化字符串的时间复杂度是多少?

如何在visionOS中将模型锚定在用户头部上方?

可以使 Swift init 仅可用于 Objective C 吗?

Swift 数组拆分成重叠的块

ScreenCaptureKit/CVPixelBuffer 格式产生意外结果

如何为等待函数调用添加超时

有没有更快的方法来循环浏览 macOS 上已安装的应用程序?

如何在 Swift 中使用子定义覆盖父类扩展

将基于MyProtocol的泛型函数的参数更改为使用存在的any MyProtocol或some MyProtocol是否会受到惩罚?

我如何从 UIAlertController 导航到新屏幕(swiftUI)

SwiftUI 文本被意外截断

Swift:在子类中覆盖 == 导致仅在超类中调用 ==

Swift 4 Decodable - 以枚举为键的字典

如何以编程方式读取 wkwebview 的控制台日志(log)

在 Swift 中指定 UITextField 的边框半径

URLComponents.url 为零