- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
导读
SE-0388(Convenience Async[Throwing]Stream.makeStream methods)为 Swift 标准库的AsyncStream与AsyncThrowingStream新增了静态工厂方法makeStream(of:bufferingPolicy:),让开发者可以一步同时获得「流」与其「续体(Continuation)」,彻底告别从初始化闭包中逃逸续体的隐式解包可选(IUO)样板代码。本文以该提案为主体,结合仓库中 SE-0314 AsyncStream 原始提案 与 SE-0406 背压演进提案 的源码级依据,讲解其动机、API 设计、实现细节、缓冲策略参数与兼容性约束,读完后你可以在 Swift 5.9+ 中直接写出更安全、更简洁的生产者/消费者并发代码。
一、背景:AsyncStream 与 Continuation 的角色划分
AsyncStream与AsyncThrowingStream由 SE-0314 引入(Swift 5.5 实现),是标准库提供的「根级 AsyncSequence」类型。它们的作用是把基于回调(callback)或委托(delegate)的多次异步产出,桥接进async/await世界:例如QuakeMonitor.quakeHandler这种每次地震事件回调一次、可被持续调用的接口,就可以包装成AsyncStream<Quake>供for await消费。
SE-0314 明确设计了两个互补的角色:
- 外层
AsyncStream<Element>:消费端,对外呈现AsyncSequence接口,通过for await或迭代器的next()逐个取回元素; - 内层
AsyncStream.Continuation:生产端,Sendable类型,可从任意并发上下文调用yield(_:)产出值、调用finish()结束序列,且可被 yield 多次。
两个角色之间的桥梁,就是初始化器接收的那个build闭包——续体通过闭包参数被"交"给生产者。SE-0314 的原始 API 签名如下(节选自 proposals/0314-async-stream.md):
public init( _ elementType: Element.Type = Element.self, bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded, _ build: (Continuation) -> Void )二、痛点:续体必须从闭包中"逃逸"出来
在实际使用中,一个常见的场景是把续体和流分别交给不同的位置:续体交给生产者(如网络层、事件源),流交给消费者(如 UI 层)。这要求把闭包参数里的Continuation逃逸(escape)出初始化闭包,而 SE-0388 提案指出这种写法存在三个层面的不便:
- 必须借助隐式解包可选(IUO):由于闭包在初始化完成后才被调用,逃逸前只能用
var cont: AsyncStream<Int>.Continuation!占位; - 触发送达性(Sendability)警告:逃逸后需要再拷贝到
let常量里才能消除警告; - 语义误导:闭包结构暗示「续体的生命周期被限定在闭包作用域内」,而实际并非如此——续体需要长期存活,直到流被 finish 或取消。
提案给出了改造前的典型写法(完整示例见 proposals/0388-async-stream-factory.md):
var cont: AsyncStream<Int>.Continuation! let stream = AsyncStream<Int> { cont = $0 } // We have to assign the continuation to a let to avoid sendability warnings let continuation = cont await withTaskGroup(of: Void.self) { group in group.addTask { for i in 0...9 { continuation.yield(i) } continuation.finish() } group.addTask { for await i in stream { print(i) } } }这段代码虽然能运行,但cont!这种"先占位、后填充"的舞步极易出错,也让"续体生命周期与闭包无关"这一事实变得模糊。
三、方案:新增makeStream静态工厂方法
SE-0388 的解决方案是在AsyncStream和AsyncThrowingStream上各新增一个静态方法makeStream,一次性返回「流 + 续体」二元组。同样是上面的任务组示例,新写法变成了:
let (stream, continuation) = AsyncStream.makeStream(of: Int.self) await withTaskGroup(of: Void.self) { group in group.addTask { for i in 0...9 { continuation.yield(i) } continuation.finish() } group.addTask { for await i in stream { print(i) } } }对比之下,makeStream方案消除了 IUO、消除了 Sendability 警告、也让"生产端与消费端各持一端"的意图一目了然。提案状态为Implemented (Swift 5.9),即从 Swift 5.9 起可用(仓库 README.md 的发布记录同样确认 Swift 5.9 于 2023-09 发布)。
四、详细设计:完整的 API 签名与实现
4.1 AsyncStream 版本
@available(SwiftStdlib 5.1, *) extension AsyncStream { /// Initializes a new ``AsyncStream`` and an ``AsyncStream/Continuation``. /// /// - Parameters: /// - elementType: The element type of the stream. /// - limit: The buffering policy that the stream should use. /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the /// producer while the stream should be passed to the consumer. @backDeployed(before: SwiftStdlib 5.9) public static func makeStream( of elementType: Element.Type = Element.self, bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded ) -> (stream: AsyncStream<Element>, continuation: AsyncStream<Element>.Continuation) { var continuation: AsyncStream<Element>.Continuation! let stream = AsyncStream<Element>(bufferingPolicy: limit) { continuation = $0 } return (stream: stream, continuation: continuation!) } }4.2 AsyncThrowingStream 版本
@available(SwiftStdlib 5.1, *) extension AsyncThrowingStream { /// Initializes a new ``AsyncThrowingStream`` and an ``AsyncThrowingStream/Continuation``. /// /// - Parameters: /// - elementType: The element type of the stream. /// - failureType: The failure type of the stream. /// - limit: The buffering policy that the stream should use. /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the /// producer while the stream should be passed to the consumer. @backDeployed(before: SwiftStdlib 5.9) public static func makeStream( of elementType: Element.Type = Element.self, throwing failureType: Failure.Type = Failure.self, bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded ) -> (stream: AsyncThrowingStream<Element, Failure>, continuation: AsyncThrowingStream<Element, Failure>.Continuation) where Failure == Error { var continuation: AsyncThrowingStream<Element, Failure>.Continuation! let stream = AsyncThrowingStream<Element, Failure>(bufferingPolicy: limit) { continuation = $0 } return (stream: stream, continuation: continuation!) } }4.3 设计要点逐项解读
- 参数
of elementType:元素类型,默认值Element.self,因此最常见的AsyncStream.makeStream(of: Int.self)与全默认调用AsyncStream.makeStream()都合法;借助类型推断,let (stream, cont) = AsyncStream<Int>.makeStream()也可以省略of:参数。 - 参数
throwing failureType(仅 Throwing 版本):失败类型,约束where Failure == Error,这与 SE-0314 中AsyncThrowingStream.init只允许Failure == Error的构造约束保持一致(见 proposals/0314-async-stream.md)。 - 参数
bufferingPolicy limit:缓冲策略,默认.unbounded,与 SE-0314 初始化器的默认行为一致。其合法取值定义在 SE-0314 的Continuation.BufferingPolicy中(见 proposals/0314-async-stream.md):
| 取值 | 行为 |
|---|---|
.unbounded | 无界缓冲,所有未被消费的 yield 值都先入缓冲区,默认值 |
.bufferingOldest(Int) | 缓冲区满时丢弃新到达的元素,保证保留最旧的 n 个值 |
.bufferingNewest(Int) | 缓冲区满时丢弃最旧的元素,保证保留最新的 n 个值 |
需要指出,AsyncStream的缓冲区只为"尚未被迭代消费"的值服务;如果缓冲容量为 0,则当没有任务正在await迭代器的next()时,yield 的值会直接被丢弃(SE-0314 的 dropping 行为)。yield的返回值YieldResult(enqueued(remaining:)/dropped(Element)/terminated)正是对这三种情况的显式回报。
- 返回值是带标签的元组
(stream:..., continuation:...):这正是评审后从"具体类型"改为"元组"的关键设计(详见下文备选方案分析)。 - 实现技巧:方法内部依然使用 IUO——
var continuation: ...!配合初始化闭包{ continuation = $0 }完成填充,再用continuation!解包返回。也就是说,SE-0388 不是"消灭"了 IUO,而是把 IUO 从用户代码收拢进标准库内部,用户侧不再接触任何强制解包。 - 注解组合:方法整体以
@available(SwiftStdlib 5.1, *)标记(沿用 SE-0314 类型自身的可用性),同时以@backDeployed(before: SwiftStdlib 5.9)标记,使得该 API 可以向后部署到旧版本 Swift 标准库上运行(详见第七节)。
五、实战:生产者/消费者模式的三种典型写法
5.1 无抛错流(AsyncStream)
let (stream, continuation) = AsyncStream.makeStream(of: Int.self) await withTaskGroup(of: Void.self) { group in group.addTask { for i in 0...9 { continuation.yield(i) } continuation.finish() } group.addTask { for await i in stream { print(i) } } }5.2 可抛错流(AsyncThrowingStream)
SE-0314 曾给出过一个把回调式"买菜"接口桥接为流的例子(见 proposals/0314-async-stream.md),用makeStream改写后,生产端可以直接把续体交给回调接口,无需闭包嵌套:
let (stream, continuation) = AsyncThrowingStream.makeStream(of: Vegetable.self) buyVegetables( shoppingList: list, onGotVegetable: { veggie in continuation.yield(veggie) }, onAllVegetablesFound: { continuation.finish() }, onNonVegetable: { error in continuation.finish(throwing: error) } ) for try await veggie in stream { // 消费蔬菜 }注意AsyncThrowingStream的消费需要使用for try await,因为迭代器next()可能抛出错误;finish(throwing:)传入的错误会被迭代器原样抛出,而finish()则表示正常结束。
5.3 处理任务取消与资源清理
makeStream返回的续体同样支持onTermination回调。SE-0314 规定(见 proposals/0314-async-stream.md):当迭代结束、流离开作用域或所在任务被取消时,会触发onTermination,其中Termination区分.finished与.cancelled两种终态。配合makeStream可写成:
let (stream, continuation) = AsyncStream.makeStream(of: Quake.self) continuation.onTermination = { termination in switch termination { case .finished: monitor.stopMonitoring() case .cancelled: monitor.stopMonitoring() } } monitor.quakeHandler = { quake in continuation.yield(quake) } monitor.startMonitoring() for await quake in stream { // 处理地震事件 }六、从源码结构看:为什么返回元组而不是具体类型
提案在 Alternatives considered 一节完整记录了设计权衡,这一决策过程对理解 API 形态至关重要。
6.1 元组 vs 具体类型
提案者最初的 pitch 使用元组作为工厂的返回类型,评审前又改成了具体类型(理由是便于写文档注释);但在正式评审中,多数反馈倾向于元组方案,最终接受时改回元组(见提案末尾 Revision history:"After review: Changed the return type from a concrete type to a tuple")。元组方案的两个主要收益:
- 引导解构(destructuring):
let (stream, continuation) = ...的写法自然地把两个值拆开,分别交给生产者与消费者各自持有——这正是期望的使用方式; - 支持向后部署(back deployment):具体类型方案无法同样轻量地实现
@backDeployed。
6.2 为什么不在init中直接传入续体
pitch 阶段有人提议让用户直接把一个续体传给AsyncStream<Element>.init(),该方案被否决,原因有二:
- 同一个续体可能被传入多个流,造成一对多、语义混乱;
- 未被任何流持有的续体毫无用处。
SE-0388 的结论是:AsyncStream.Continuation与某个AsyncStream实例深度耦合(一一对应),因此 API 应当把这种耦合关系直接表达出来,从类型层面杜绝误用。
6.3 为什么不"什么都不做"
提案也考虑了维持现状的选项,但认为既然AsyncStream属于标准库,就应该提供一个更体面的方式来同时创建流与续体,而非要求用户手写 IUO 逃逸样板。
七、兼容性与演进:source compatibility、ABI 与 API resilience
SE-0388 对兼容性的影响全部是纯增量的:
- Source compatibility:新增静态方法,不修改任何既有声明,对源码完全无影响;
- Effect on ABI stability:仅引入新的并发库 ABI(
makeStream方法本身),不影响既有声明的 ABI; - Effect on API resilience:无影响,既有弹性模型允许添加静态方法;
- 向后部署:
@backDeployed(before: SwiftStdlib 5.9)意味着在 Swift 5.9 之前的标准库运行时上,编译器会在调用点内联该方法的实现,使旧系统也能获得这一便捷 API。
八、后续演进:SE-0406 中带背压的makeStream变体
makeStream的工厂形态并非终点。仓库中的 SE-0406 AsyncStream backpressure 在其上进一步演进:引入makeStream(of:backpressureStrategy:),返回(stream, source),source提供write(contentsOf:)、enqueueCallback等带背压的写入 API,支持.watermark(low:high:)等策略(见 proposals/0406-async-stream-backpressure.md):
let (stream, source) = AsyncStream.makeStream( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) )SE-0406 还明确将其定位为严格单播(strict unicast)异步序列,并讨论了makeStream方法中提供onTerminate回调的方案。这说明「一次性返回流 + 生产端」这一工厂模式已成为 Swift 异步流 API 的标准骨架,后续演进均在此基础上叠加能力,而 SE-0388 正是这一模式的奠基者。
九、总结
SE-0388 的makeStream以极小的 API 面(两个静态方法)解决了AsyncStream使用中最常见的痛点:续体逃逸样板。它把隐式解包可选收敛进标准库实现、用元组返回引导正确的解构语义、并以@backDeployed实现跨版本可用。在 Swift 5.9 及以后,let (stream, continuation) = AsyncStream.makeStream(of: Element.self)应成为所有生产者/消费者桥接代码的默认起点;若你需要更精细的背压控制,则可进一步关注 SE-0406 的演进方案。相关完整资料可继续阅读仓库内的 SE-0388 原文、SE-0314 AsyncStream 设计 与 SE-0406 背压演进。
- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
相关推荐
Swift Clock 的纪元(Epoch)体系:深入解读 SE-0473 的 `systemEpoch` 设计
Swift Clock 的纪元(Epoch)体系:深入解读 SE 0473 的 systemEpoch 设计 导读 :SE 0473《Clock Epochs》
文档DDD示例工厂:深入理解领域驱动设计的实战之旅
DDD示例工厂:深入理解领域驱动设计的实战之旅 项目介绍 DDD(Domain Driven Design,领域驱动设计)通过强调业务领域和软件开发之间的紧密结
Swift 标准库演进解读:SE-0218 Dictionary.compactMapValues 的设计、实现与实战
Swift 标准库演进解读:SE 0218 Dictionary.compactMapValues 的设计、实现与实战 本文以 Swift Evolution
文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考