我在SWIFT中有一个缓慢的阻塞函数,我想以非阻塞(异步/等待)的方式调用它.

以下是原始的阻止代码:

// Original
func totalSizeBytesBlocking() -> UInt64 {
    var total: UInt64 = 0
    for resource in slowBlockingFunctionToGetResources() {
        total += resource.fileSize
    }
    return total
}

以下是我让它成为非阻塞的try :

// Attempt at async/await
func totalSizeBytesAsync() async -> UInt64 {
    // I believe I need Task.detached instead of just Task.init to avoid blocking the main thread?
    let handle = Task.detached {
        return self.slowBlockingFunctionToGetResources()
    }
    // is this the right way to handle cancellation propagation with a detached Task?
    // or should I be using Task.withTaskCancellationHandler?
    if Task.isCancelled {
        handle.cancel()
    }
    let resources = await handle.value
    var total: UInt64 = 0
    for resource in resources {
        total += resource.fileSize
    }
    return total
}

我的目标是能够等待来自主参与者/线程的异步版本,并让它在后台线程上执行缓慢的阻塞工作,同时继续更新主线程上的UI.

这应该怎么做呢?

推荐答案

这里有几个问题.

  1. How to handle cancelation?

    你的怀疑是正确的,即这种Task.isCancelled模式是不够的.问题是,您在创建任务后立即进行测试,但它将快速通过取消判断,然后在任务的第await处暂停,此时将不会检测到进一步的取消.正如您所猜到的,在处理unstructured concurrency时,您可以使用withTaskCancellationHandler,而不是:

    func totalSizeBytesAsync() async throws -> UInt64 {
        let task = Task.detached {
            try self.slowBlockingFunctionToGetResources()
        }
        let resources = try await withTaskCancellationHandler {
            try await task.value
        } onCancel: {
            task.cancel()
        }
    
        return resources.reduce(0) { $0 + $1.fileSize }
    }
    
    func slowBlockingFunctionToGetResources() throws -> [Resource] {
        var resources: [Resource] = []
        while !isDone {
            try Task.checkCancellation()
            resources.append(…)
        }
        return resources
    }
    

    也许不用说,如上所述,只有当慢速阻止功能支持它时,取消才会正确地工作(例如,周期性地try checkCancellation次,或者,不太理想的,测试isCancelled).

    如果这个慢同步函数不能处理取消,那么判断取消就没有什么意义了,并且您将被一个直到同步任务完成才能完成的任务所困.但至少有totalSizeBytesAsync个不会阻塞.例如:

    func totalSizeBytesAsync() async -> UInt64 {
        let resources = await Task.detached {
            self.slowBlockingFunctionToGetResources()
        }.value
    
        return resources.reduce(0) { $0 + $1.fileSize }
    }
    
    func slowBlockingFunctionToGetResources() -> [Resource] {…}
    
  2. Should you use unstructured concurrency at all?

    作为一般规则,我们应该避免用unstructured concurrency来杂乱我们的代码(我们要承担手动判断取消的负担).因此,它回避了一个问题,即如何在保持 struct 化并发的同时从当前参与者那里获得任务.您可以将同步函数放在它自己的单独参与者中.或者,因为SE-0338,你可以 Select 让你的慢速功能既有nonisolated又有async.从现在的演员身上脱颖而出:

    func totalSizeBytesAsync() async throws -> UInt64 {
        try await slowBlockingFunctionToGetResources()
            .reduce(0) { $0 + $1.fileSize }
    }
    
    nonisolated func slowBlockingFunctionToGetResources() async throws -> [Resource] {
        var resources: [Resource] = []
        while !isDone {
            try Task.checkCancellation()            
            resources.append(…)
        }
        return resources
    }
    

    但是,通过保持 struct 化并发,我们的代码大大简化了.

    显然,如果你想用actor,请随意:

    let resourceManager = ResourceManager()
    
    func totalSizeBytesAsync() async throws -> UInt64 {
        try await resourceManager.slowBlockingFunctionToGetResources()
            .reduce(0) { $0 + $1.fileSize }
    }
    

    在哪里:

    actor ResourceManager {
        …
    
        func slowBlockingFunctionToGetResources() throws -> [Resource] {
            var resources: [Resource] = []
            while !isDone {
                try Task.checkCancellation()
                resources.append(…)
            }
            return resources
        }
    }
    
  3. How slow is the synchronous function?

    SWIFT并发性依靠"契约"来避免阻塞协作线程池中的任何线程.请参见https://stackoverflow.com/a/74580345/1271826.

    因此,如果这真的是一个缓慢的函数,我们真的应该让我们的缓慢进程定期Task.yield()到SWIFT并发系统,以避免潜在的死锁.例如,

    func totalSizeBytesAsync() async throws -> UInt64 {
        try await slowNonBlockingFunctionToGetResources()
            .reduce(0) { $0 + $1.fileSize }
    }
    
    nonisolated func slowNonBlockingFunctionToGetResources() async throws -> [Resource] {
        var resources: [Resource] = []
        while !isDone {
            await Task.yield()
            try Task.checkCancellation()
            resources.append(…)
        }
        return resources
    }
    

    现在,如果(A)您没有机会将此函数重构到定期yield;以及(B)它真的非常、非常慢,那么Apple建议您将此函数从SWIFT并发系统中删除.例如,在WWDC 2022视频Visualize and optimize Swift concurrency中,他们建议GCD:

    如果您有需要执行这些操作的代码[不能定期屈服于SWIFT并发系统的缓慢的同步函数],请将这些代码移到并发线程池之外--例如,通过在调度队列上运行它--并使用延续将其连接到并发世界.尽可能使用异步API进行阻塞操作,以保持系统平稳运行.

    归根结底,要小心在任何长时间内阻塞协作线程池线程.

Swift相关问答推荐

RealityKit如何获得两个相对大小相同的ModelEntity?

如何在visionOS上删除PhotosPicker背景 colored颜色 ?

如何防止自动状态恢复,如果应用程序被启动打开特定的文档?

如何使用AsyncTimerSequence获取初始时钟,然后在指定的时间间隔内开始迭代?

如何在SwiftUI 4+(iOS 16+)中使用新的NavigationStack进行导航

如何获取嵌套数组中项的路径以修改数组?

除法中如果除以完全因数需要更长时间吗?

为什么我的Swift app不能用xcodebuild构建,但是用XCode可以构建?

Swift 并发任务与调度队列线程:是什么决定同时运行多少任务?

ConfirmationDialog取消swiftui中的错误

覆盖一个子元素的 HStack 对齐方式

如何裁剪图像 3:4 ?迅速

如何使用 ISO 国家代码(2 个字母)查找区域设置标识符

Swiftwhere数组扩展

Swift 中 NSFetchRequest 的多个 NSPredicates?

由于编译器中的内部保护级别,无法访问框架 init 中的公共 struct

SwiftUI - 呈现工作表后导航栏按钮不可点击

Swift 2.0 中的 do { } catch 不处理从这里抛出的错误

<<错误类型>> 奇怪的错误

如何判断 firebase 数据库值是否存在?