首页 / Swift 编程语言教程 / 异步序列 AsyncSequence

Swift 编程语言教程

异步序列 AsyncSequence

本教程共 93 篇 · 第 86 篇 · 更新于 2026-08-08 · 约 7 分钟阅读

SwiftSwift 编程语言教程并发AsyncSequence异步序列for await流式数据协议

本节目标:理解异步序列如何像 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-catchtry 兜住。

Note

取消在异步序列上的传播,是结构化并发”取消沿任务树向下”的又一体现。你开的 for await 循环,本质也是任务树里的一环,自然享受统一的取消语义。

1-9 小结

异步序列是普通序列的异步版,用 for await 逐条消费异步产生的数据,语法贴近 for-in 只是多一个 await。它适合流式、持续产生、可能无限的数据。自定义时让类型遵循 AsyncSequence,提供异步的 next(),返回 nil 表示结束。

至此并发的语法工具你基本集齐:async/await、async let 与任务组、Task 与取消、actor 隔离、Sendable、主 actor、异步序列。最后一章把这些串起来,讲并发的陷阱与最佳实践。