云端 Mac 上的 Swift 回调转异步验收:重复完成与取消竞态

云端 Mac 上的 Swift 回调转异步验收:重复完成与取消竞态

旧 SDK 的完成回调在本机测试一直正常,迁到云端 Mac 并行执行后,却偶发 continuation 重复恢复,或者任务取消后一直等不到结果。问题通常不在机器性能,而在适配器把“回调只来一次、一定异步到达”当成了事实。下面把这两个假设移除,用独立 Swift 程序验收边界,不依赖真实网络或随机等待。

先定义谁能决定最终结果

本文处理一次性操作:成功、失败、取消三者中,先提交到状态机的结果生效。后续通知不再改变结果,也不能再次恢复等待方。流式、多次更新的接口不适用这个契约。

需要保存两类状态:尚未安装的 continuation,以及已经确定的最终结果。取消可能先于 continuation 安装,因此不能仅靠一个可空变量判断任务是否结束。

checked continuation 会检查错误用法,但不会替你去重,也不会自动把任务取消转成底层操作取消。

这里采用“先提交者生效”,不是“取消永远优先”。成功已经提交后,即使恢复尚未被调度执行,随后到达的取消也不能覆盖成功。业务若要求更严格的取消优先级,应另行定义提交点,而不是在返回值外面随意补检查。

用短临界区保护完成状态

将下面代码保存为 BridgeCheck.swift。样例使用 Swift 6,以及提供 NSLock.withLock 的 macOS SDK。锁只保护状态读取和写入,不包住外部回调、请求启动或 continuation 恢复。

import Foundation

final class CompletionGate<Value: Sendable>: @unchecked Sendable {
    private let lock = NSLock()
    private var outcome: Result<Value, any Error>?
    private var waiter: CheckedContinuation<Value, any Error>?

    func install(_ continuation: CheckedContinuation<Value, any Error>) {
        let ready: Result<Value, any Error>? = lock.withLock {
            if let outcome {
                return outcome
            }
            precondition(waiter == nil)
            waiter = continuation
            return nil
        }
        if let ready {
            continuation.resume(with: ready)
        }
    }

    func finish(_ result: Result<Value, any Error>) {
        let continuation: CheckedContinuation<Value, any Error>? =
            lock.withLock {
                guard outcome == nil else {
                    return nil
                }
                outcome = result
                let current = waiter
                waiter = nil
                return current
            }
        continuation?.resume(with: result)
    }
}

func bridge(
    start: @Sendable (
        @escaping @Sendable (Result<Int, any Error>) -> Void
    ) -> Void
) async throws -> Int {
    let gate = CompletionGate<Int>()
    return try await withTaskCancellationHandler {
        try await withCheckedThrowingContinuation {
            (continuation: CheckedContinuation<Int, any Error>) in
            gate.install(continuation)
            start { result in
                gate.finish(result)
            }
        }
    } onCancel: {
        gate.finish(.failure(CancellationError()))
    }
}

@unchecked Sendable 不是关闭检查后就不管了。它表示维护者承诺:共享可变字段全部由同一把锁保护,结果值满足 Sendable。以后新增计数器或句柄,也必须审查其同步方式。

安装与完成之间只有两种次序:先安装就取出等待方恢复;先完成就保存结果,等安装时恢复。清空等待方可以及时解除引用,但“是否结束”的依据始终是结果状态,不能把清空误判为重新开放。

用确定性样例覆盖关键次序

把以下入口追加到同一个文件。第一项故意同步回调两次;第二项启动一个永不回调的操作,随后取消等待方。整个过程没有用休眠猜测调度时机。

@main
struct BridgeCheck {
    static func main() async throws {
        let value = try await bridge { complete in
            complete(.success(7))
            complete(.success(9))
        }
        precondition(value == 7)

        let pending = Task {
            try await bridge { _ in }
        }
        pending.cancel()

        do {
            _ = try await pending.value
            preconditionFailure("Expected cancellation")
        } catch is CancellationError {
        }
    }
}
xcrun swiftc -parse-as-library -swift-version 6 \
  BridgeCheck.swift -o BridgeCheck
./BridgeCheck

正常执行应以退出码零结束。同步完成用例能抓出“先启动、后存 continuation”的错误实现;重复通知用例能抓出无保护的第二次恢复;取消用例则检查没有回调时等待方能否结束。

接入项目测试后,还应直接测试状态机的“先完成、后安装”,并把成功与失败按相反顺序各跑一次。并发压力测试可以补充覆盖,但不能替代这些明确指定顺序的用例。

取消等待不代表停止底层操作

这个适配器保证等待方结束,却没有停止底层工作。取消若先到,start 仍可能执行,随后通知会被忽略。这是有意保留的边界,不应把样例直接当作完整网络客户端。

有取消句柄时怎样扩展

真实接口若返回取消句柄,需要在同一个受保护状态里登记它。取消先到时记录标记;句柄稍后返回时立即取出并取消。句柄先到时,则由取消路径取出。实际取消动作仍在锁外执行,因为它可能同步触发完成回调。

不要只在启动前检查一次取消状态。检查通过与句柄返回之间仍存在竞态。也不要在锁内调用取消接口,否则同步回调再次进入状态机时可能死锁。

若底层永不通知、调用方也不取消,等待仍会持续。业务超时必须走同一个完成入口,并同时处理底层资源回收;不能假设 continuation 自带超时。

把验收放进云端执行入口

在云端 Mac 的构建前置阶段编译并运行这份检查,记录编译器版本、提交标识和退出码。样例不需要图形会话,适合通过命令行重复执行。

合并前核对四项:每次调用独享一个状态机;同步回调不会丢失;重复通知不会重复恢复;取消后没有等待方悬挂。对带句柄的实际接口,再补“取消早于句柄返回”的专门测试。

最后检查闭包持有关系。即使等待已经结束,底层仍可能保留完成闭包;状态机去重不能解决所有生命周期问题。先把完成契约、停止契约和资源释放分别验收,异步包装才算真正收口。

常见问题

checked continuation 会自动忽略第二次回调吗?

不会。重复 resume 属于错误用法,可能触发运行时检查。应在回调入口通过同步保护的状态机选出唯一完成结果,再在锁外恢复 continuation。

任务取消后,底层请求也会停止吗?

不会自动停止。本文适配器只让等待方以取消错误结束;底层操作仍需调用自己的取消接口,并处理取消早于句柄返回的情况。

把下一次构建交给独享云端 Mac

MacFTP 提供运行原生 macOS 的 Apple Silicon 独享物理机,按天、周、月、季选择租期。先确认配置,再将现有工具链接入节点;实际可用状态以控制台实时返回为准。

立即租用云端 Mac

从阅读回到项目

先核对配置,再安排交付

比较机型与租期,或返回博客继续查阅技术记录。