Swift 异步序列 AsyncStream 新“玩法”以及内存泄漏、死循环那些事儿(上)
在本篇博文中,我们讨论了 Swift 5.5 新并发模型中用 AsyncStream 结构创建异步序列的新方法,并比较了它和之前旧的实现有哪些进步。
概览
异步序列(Async Sequence)是 Swift 5.5 新并发模型中的一员“悍将”,系统标准库中很多类都做了重构以支持异步序列。我们还可以用 AsyncStream 辅助结构非常方便的创建自己的异步序列。
这里我们就来一起聊聊 AsyncStream 结构,以及它新增的 makeStream 构建器方法。
在本篇博文中,您将学到如下内容
而在下篇中,我们将再接再厉继续讨论异步序列在使用时可能产生的内存泄漏、无限循环等等那些的潜伏陷阱。
相信学完本系列课程后,大家会对 Swift 新异步并发模型中异步序列的正确使用有更为深刻的领悟。
那还等什么呢?Let‘s find out!!!😉
1. AsyncStream 旧构造器的弊端
在 Swift 中创建自定义异步序列有很多种“姿势”,其中一个常见的方法是使用 AsyncStream 结构,可以认为它是一个异步序列的辅助构造器:
我们知道异步序列中的核心和精髓就是它的 Continuation 对象,做一个“二次元卡哇伊”的比喻:如果异步序列是一只大螃蟹,则 Continuation 就是它肥得流油的“蟹黄”:
值得注意的是,不像 Swift 中其它连续体(Continuation)对象,AsyncStream.Continuation 支持可逃逸(escaping)特性。这就让它的使用灵活性更上了一个层次。
我们使用 AsyncStream 创建异步序列主要有两种场景,一种是直接在其创建时就“包办”固定好所有元素的产出,但这样做缺乏变数、比较“死板”:
let stream = AsyncStream(unfolding: {
return Int.random(in: 0..<Int.max)
})
另一种场景多半被用在 Apple 开发中的代理(Delegate)模式中,这种方式更加灵动自如:
protocol NumberSpawnerDelegate {
func spawn(_ numbers: [Int])
}
struct Spawner {
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
var delegator: NumberSpawnerDelegate?
var cancel: Cancellable?
mutating func setup() {
cancel = timer.sink { [self] _ in
var numbers = [Int]()
for _ in 0..<Int.random(in: 1...3) {
numbers.append(Int.random(in: 0...10000))
}
self.delegator?.spawn(numbers)
}
}
}
class AsyncNumberStream: NumberSpawnerDelegate {
var continuation: AsyncStream<Int>.Continuation?
lazy var stream: AsyncStream<Int> = {
AsyncStream { continuation in
self.continuation = continuation
}
}()
func spawn(_ numbers: [Int]) {
for i in numbers {
continuation?.yield(i)
}
}
}
如上代码所示,我们的 AsyncNumberStream 异步序列遵从于 NumberSpawnerDelegate 协议,而 Spawner 作为驱动者自然就成为了 AsyncNumberStream 的事件源,它通过调用协议中的 spawn(😃 方法连接了发布者和接受者,使得天堑变通途。
我们可以这样使用 AsyncNumberStream 异步序列:
Task {
let stream = AsyncNumberStream()
var spawner = Spawner()
spawner.delegator = stream
spawner.setup()
for await i in stream.stream {
print("\(i)")
}
}
运行结果如下所示:
不过这种以 AsyncStream 构造器“抓取”其 Continuation 对象的方式略显别扭(合肥话叫“肘手”)。而且 continuation 属性类型需要设置为可选值(AsyncStream<Int>.Continuation?),这多少让人觉得有些“不畅快”。
2. 拯救者:新方法 makeStream!
从 iOS 17.0 开始 Apple 为 AsyncStream 添加了一个新的 makeStream 方法专门用来解决上述窘境:
值得注意的是,虽然 makeStream 在 iOS 17 才被加入,但它向后兼容旧的系统(iOS 13 - iOS 17),所以在之前的 iOS 中也可以任性的使用它。
该方法返回一个由异步序列和其对应连续体组成的元组:
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@_backDeploy(before: macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0)
public static func makeStream(of elementType: Element.Type = Element.self, bufferingPolicy limit: AsyncStream<Element>.Continuation.BufferingPolicy = .unbounded) -> (stream: AsyncStream<Element>, continuation: AsyncStream<Element>.Continuation)
这意味着之前“肘手”的调用可以改成这样:
class AsyncNumberStream: NumberSpawnerDelegate {
let stream: AsyncStream<Int>
private let continuation: AsyncStream<Int>.Continuation
init() {
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
self.stream = stream
self.continuation = continuation
}
func spawn(_ numbers: [Int]) {
for i in numbers {
continuation.yield(i)
}
}
}
从上面代码可以看到,AsyncStream.makeStream 方法带来了如下一些改变:
- Continuation 不再“嵌入”在 AsyncStream 构造器的回调闭包之中,它们现在处在同一个层级;
- continuation 属性不再要求是可选类型了;
- 整体实现更加简单、一目了然;
现在,我们对 AsyncStream.Continuation 的获取不再聱牙诘屈,同时也完美的消除了 continuation 属性可选类型的限制,正谓是一举两得、一石二鸟也!
当然,可能有的小伙伴们觉得 AsyncStream.makeStream 方法如下形式的调用更加 nice 一些:
init() {
let result = AsyncStream.makeStream(of: UUID.self)
locations = result.stream
continuation = result.continuation
}
值得一提的是,尽管我们将 AsyncNumberStream 内部的逻辑“粉饰一新”,但外部接口并没有丝毫改变。所以,之前的调用无需做任何修改。
编译运行代码可以发现,一切都未曾改变,正所谓平平淡淡才是真!棒棒哒!
虽然新的 makeStream 方法让我们原有的实现“清风徐来,水波不兴”,但异步序列本身的使用仍然暗影重重、波诡云谲。康庄大道上还有很多陷阱等着算计我们,我们将在下篇博文中将它们一网打尽!
总结
在本篇博文中,我们讨论了 Swift 5.5 新并发模型中用 AsyncStream 结构创建异步序列的新方法,并比较了它和之前旧的实现有哪些进步。
在下篇博文中,我们将继续异步序列的填坑之旅,期待吧!
感谢观赏,再会!😎

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)