旧 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 独享物理机,按天、周、月、季选择租期。先确认配置,再将现有工具链接入节点;实际可用状态以控制台实时返回为准。