异步序列 AsyncSequence
本教程共 93 篇 · 第 86 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:理解异步序列如何像 for-in 一样逐条消费异步产生的数据,掌握 for await 语法,以及怎么让自己的类型也提供异步序列。
前面学的异步函数,都是”等所有数据齐了,一次性返回整个数组”。但现实里很多数据是一股一股来的:读取大文件是一行一行吐、网络流是一块一块到、传感器是一个一个报。这种”边产生边消费”的场景,用异步序列(AsyncSequence)最合适。
1-1 普通序列的异步版
你已经熟悉普通 for-in 遍历一个 Sequence(数组、区间等)。异步序列就是把”取下一个元素”这一步变成可以挂起的。写法上,只是在 for 后面加个 await:
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
handle.bytes.lines 是一个异步序列,每次循环取一行。因为”等下一行到来”可能要挂起,所以写 for try await——try 是因为读可能抛错,await 是因为要等。每一轮循环开头都可能是挂起点,等下一行就绪才继续。
Note注意区别:普通
for-in的元素是”已经在内存里”的;for await的元素是”将来某个时刻才产生”的。二者遍历语法几乎一样,只是后者承认了”等”的事实。
1-2 和标准 Sequence 的对照
体会一下两者的对称美:
| 普通序列 Sequence | 异步序列 AsyncSequence |
|---|---|
for x in collection { } | for await x in asyncCollection { } |
| 元素立刻可取 | 取下一个元素可能挂起 |
遵循 Sequence 协议 | 遵循 AsyncSequence 协议 |
| 用于已有数据 | 用于流式/持续产生的数据 |
这种对称不是巧合。Swift 刻意让异步序列的写法贴近普通序列,降低学习成本。你只要把”遍历”的直觉搬过来,加个 await 就行。
1-3 异步序列的常见来源
标准库和框架里异步序列随处可见。除了上面读文件的 lines,还有:
- 网络下载的字节流,逐块到达。
- 通知中心的事件流,事件一个接一个来。
- 计时器,每隔一段时间产生一个时刻。
- 你自定义的、任何”按需产生多个异步值”的来源。
当你发现”我想一遍遍拿到异步产生的值”时,就该想到 AsyncSequence,而不是笨拙地反复调异步函数拼数组。
1-4 自定义一个 AsyncSequence
想让自己的类型也能被 for await 遍历,就让它遵循 AsyncSequence。核心是实现 makeAsyncIterator(),返回一个遵循 AsyncIteratorProtocol 的迭代器,其 next() 是异步的:
struct Countdown: AsyncSequence {
typealias Element = Int
let start: Int
func makeAsyncIterator() -> CountdownIterator {
CountdownIterator(start: start)
}
struct CountdownIterator: AsyncIteratorProtocol {
var current: Int
init(start: Int) { self.current = start }
mutating func next() async -> Int? {
guard current > 0 else { return nil }
let value = current
current -= 1
try? await Task.sleep(for: .seconds(1))
return value
}
}
}
next() 返回 Int?:有值就返回,返回 nil 表示序列结束(这是所有序列的通用约定)。这里每次 next() 还 sleep 一秒,模拟”每秒产生一个数”。
1-5 遍历自定义异步序列
用法和遍历标准库异步序列一模一样:
Task {
for await number in Countdown(start: 3) {
print(number) // 每隔约 1 秒打印 3、2、1
}
print("倒计时结束")
}
for await number in Countdown(start: 3) 会反复调用迭代器的 next(),每次等它返回。返回 nil 时循环自然结束。整个过程异步、流式、优雅。
Tip自定义异步序列时,
next()返回nil是终止信号,务必在合适条件(这里是current <= 0)返回,否则会陷入死循环,永远不结束。
1-6 为什么不用数组一次性返回
有人会问:我直接在异步函数里攒个数组返回不行吗?对”数据量小、已知全部”的情况当然行。但异步序列在三种场景更优:
第一,数据量巨大或无限(比如实时传感器流),根本装不进一个数组。
第二,你想”来一个处理一个”,不必等全部到齐,延迟更低、体验更顺。
第三,处理中途可能想取消(配合任务取消),异步序列天然配合结构化并发的取消传播。
所以异步序列不是数组的替代品,而是”流式”这个语义的正确建模。
Warning
for await循环本身也是可被取消的。如果外层任务被取消,正在等待下一个元素的循环会抛出取消错误并退出。设计流式处理时要考虑到这一点,别假设序列总会跑到nil。
1-7 把异步函数包成异步序列
如果你想把一个”一次给一个值”的异步过程,包装成可被 for await 遍历的序列,可以用标准库提供的 AsyncStream。它帮你省去手写迭代器协议样板:
func numberSource() -> AsyncStream<Int> {
AsyncStream { continuation in
Task {
for i in (1...3).reversed() {
try? await Task.sleep(for: .seconds(1))
continuation.yield(i) // 产出一个值
}
continuation.finish() // 结束序列
}
}
}
Task {
for await n in numberSource() {
print(n) // 每隔 1 秒:3、2、1
}
}
continuation.yield(_:) 负责”吐出一个元素”,continuation.finish() 表示序列结束(对应 next() 返回 nil)。AsyncStream 把”生产”和”迭代”解耦,是实现自定义异步序列时最省事的选择。
Note
AsyncStream适合”手动控制每个值的产出时机”的场景。若数据来源本身就是一个标准异步序列(如文件行、通知流),直接遍历即可,不必自己包。
另外补充一点:AsyncStream 还有带缓冲策略的变体,能控制”生产者快、消费者慢”时最多暂存几个值,避免无限堆积占用内存。对高频数据流,这个细节值得你之后深入了解。
1-8 异步序列与任务取消的配合
再强调一个要点:遍历异步序列的 for await 循环本身是可取消的。如果承载它的任务被取消,循环在等待下一个元素时会收到取消信号并退出,而不是傻等到底。
这意味着你可以用异步序列优雅地实现”可中断的流式处理”。比如监听一个事件流,用户一取消,循环自然停,不用额外写轮子。但也提醒你:不要假设异步序列一定会走到 nil 才结束——它可能因为取消而提前以抛错方式退出,调用方要用 do-catch 或 try 兜住。
Note取消在异步序列上的传播,是结构化并发”取消沿任务树向下”的又一体现。你开的
for await循环,本质也是任务树里的一环,自然享受统一的取消语义。
1-9 小结
异步序列是普通序列的异步版,用 for await 逐条消费异步产生的数据,语法贴近 for-in 只是多一个 await。它适合流式、持续产生、可能无限的数据。自定义时让类型遵循 AsyncSequence,提供异步的 next(),返回 nil 表示结束。
至此并发的语法工具你基本集齐:async/await、async let 与任务组、Task 与取消、actor 隔离、Sendable、主 actor、异步序列。最后一章把这些串起来,讲并发的陷阱与最佳实践。