Swift async/await 并发代码中的偶尔崩溃 - 仅在发布版本中

Occasional crash in Swift async/await concurrency code - only in release builds

我在使用 Swift 的新并发功能的某些代码中偶尔会发生崩溃。这种崩溃似乎永远不会发生在开发构建中,无论是在模拟器中还是当我直接从 Xcode 在设备上安装代码时。然而,当人们从 TestFlight 安装代码时,这种情况经常发生。

实际崩溃是这样的:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000040
Exception Codes: 0x0000000000000001, 0x0000000000000040

Thread 6 name:
Thread 6 Crashed:
0   libswift_Concurrency.dylib          0x00000001e6df7440 swift::TaskGroup::offer(swift::AsyncTask*, swift::AsyncContext*) + 504 (TaskGroup.cpp:603)
1   libswift_Concurrency.dylib          0x00000001e6df3c5c swift::AsyncTask::completeFuture(swift::AsyncContext*) + 132 (Task.cpp:180)
2   libswift_Concurrency.dylib          0x00000001e6df54d0 completeTaskAndRelease(swift::AsyncContext*, swift::SwiftError*) + 128 (Task.cpp:323)
3   libswift_Concurrency.dylib          0x00000001e6df5425 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1 (Task.cpp:365)

我不确定这对应于哪一行代码,但它可能在此处的某处:

class MyClass {

    var results: [String: Set<String>] = [:]

    func getSingleResult(index: Int) async -> (String, Set<String>) {
        // ....
    }

    func getAllResults(range: ClosedRange<Int>) async -> [String: Set<String>] {
        await withTaskGroup(
                of: (String, Set<String>).self,
                returning: [String: Set<String>].self
            ) { [self] group in
                for i in range {
                    group.addTask { await getSingleResult(index: i) }
                }

                var results: [String: Set<String>] = [:]

                for await result in group {
                    results[result.0] = result.1
                }

                return results
            }
    }
    
    func blockingDoWork(range: ClosedRange<Int>) {
        results = [:]

        let dispatchGroup = DispatchGroup()
        dispatchGroup.enter()

        Task.init {
            results = await getAllResults(range: range)
            dispatchGroup.leave()
        }
        
        dispatchGroup.wait()

        // Now do something with `results`
    }

我正在尝试弥合同步代码和异步代码之间的鸿沟(也许是错误的方式)。基本上我有一个 thread/synchronous 代码,然后创建可变数量的异步调用并尝试阻塞直到所有这些调用完成。它在 results class 成员中聚合这些调用的结果,这可能不是最好的方法,但却是我可以让异步代码与同步代码进行通信的唯一方法。

此代码在开发版本中似乎 运行 正常,在发布版本中 运行 1000 次但随后崩溃。

我似乎无法打开线程清理器或地址清理器,因为我的 Xcode 项目使用 Swift 包管理器,并且在使用这两个时存在导致构建失败的错误.

知道可能出了什么问题吗?我假设我对开发版本很幸运,并且我在这段代码中遇到了一些基本问题,但我对 Swift 的新并发功能的微妙之处知之甚少,无法识别它。

不能将信号量与 async-await 结合使用。见 Swift concurrency: Behind the scenes:

[Primitives] like semaphores ... are unsafe to use with Swift concurrency. This is because they hide dependency information from the Swift runtime, but introduce a dependency in execution in your code. Since the runtime is unaware of this dependency, it cannot make the right scheduling decisions and resolve them. In particular, do not use primitives that create unstructured tasks and then retroactively introduce a dependency across task boundaries by using a semaphore or an unsafe primitive. Such a code pattern means that a thread can block indefinitely against the semaphore until another thread is able to unblock it. This violates the runtime contract of forward progress for threads.

您可以考虑在同一视频中使用 LIBDISPATCH_COOPERATIVE_POOL_STRICT 环境变量作为 discussed here 进行测试。

你问:

I'm trying to bridge the divide between synchronous and asynchronous code (perhaps the wrong way).

您应该重构调用此同步方法的代码以采用异步模式,然后删除所有阻塞API(例如,信号量wait、调度组wait等。 ).这些在 GCD 世界中是 anti-patterns,并且在 Swift 并发性中应避免。我明白为什么不熟悉异步编程的开发人员会被那些同步的如此吸引anti-patterns,但它一直是一个错误,应该从代码中剔除。

最重要的是,在 Swift 并发中,必须“维护线程始终能够向前推进的运行时契约”。只要接受异步模式(即,保持在 async-await 内,没有任何 old-school thread-blocking 技术),你应该是好的。

FWIW,Swift concurrency: Update a sample app 展示了增量更新旧应用程序的有趣技术。例如,将此阻塞方法标记为已弃用,然后编译器会警告您调用它的位置,您可以将重构工作指向那些有问题的例程。