【译】异步迭代语义(二)

182 阅读6分钟

原文链接:https://blog.yoshuawuyts.com/async-iteration/

原文标题:ASYNC ITERATION SEMANTICS

公众号:Rust 碎碎念

并行迭代有多常见?(HOW COMMON IS PARALLEL ITERATION?)

支持异步迭代默认使用并行语义的一个重要论点是,它是最普遍需要的语义。或者在今天某些特定领域内是这样,但是在将来异步 Rust 覆盖的全部领域内的真实性如何呢?

如果我们想了解异步 Rust 可能的发展方向,我们可以看一下 Node.js streams: 他们的存在时间比 Rust strems 的存在时间长了十年。在 Node.js 生态系统中的一种常见类型的 stream 是transform stream[1]:它接收一个输入的 stream 并产生一个输出的 stream。以csv-parser package[2] 为例:它接收一个 CSV 文本的 stream,并且产生一个被解析过的元素的 stream:

NAME,AGE
Daffy Duck,24
Bugs Bunny,22
const csv = require('csv-parser')
const fs = require('fs')
const results = [];

fs.createReadStream('data.csv')
  .pipe(csv())
  .on('data', (data) => results.push(data))
  .on('end', () => {
    console.log(results);
    // [
    //   { NAME: 'Daffy Duck', AGE: '24' },
    //   { NAME: 'Bugs Bunny', AGE: '22' }
    // ]
  });

把这段代码翻译到异步的 Rust(使用异步迭代语法)中,我们会得到下面的代码:

use async_std::fs::File;
use csv_parser::Csv;
use std::io;

async fn main() -> io::Result<()> {
    let file = File::open("data.csv").await?
    for item.await? in Csv::new(file) {
        println!("name: {}, age: {}", item["NAME"], item["AGE"]);
    }
}

在这种情况下,吞吐量很可能会受到我们从磁盘读取速度的限制,这意味着为内部逻辑生成任务很可能不会加快这个速度。事实上,为每一项生成一个任务实际上可能会降低解析器的速度,因为“读取文件”和“操作文件”不再被编译成一个单独的状态机。 尽管任务相较于线程开销较低,但是这并不意味着没有开销。

上面的例子可以并行化,如果说,我们正在处理一个文件目录,我们就可以为每个文件生成一个任务。但每个文件本身仍将作为一个串行 stream 来处理。

串行 stream 语义的另一个例子是 HTTP/1.1 协议:即将到来的 TCP 连接 stream 是并行的,但将 TCP stream 转换成 HTTP 请求的序列是串行的。而在请求本身中,每个 HTTP 体也是串行的。即使是像 HTTP/2 这样的复用协议,最终也会分解成具有串行语义的请求/响应对:

async fn main() -> io::Result<()> {
    // parallel
    for stream.await? in TcpListener::accept("localhost:8080") {
        // parallel
        for req.await? in Http2Listener::connect(stream) {
            // sequential
            for (i, line?).await in req.lines() {
                println!("line: {}, value: {}", i, line);
            }
        }
    }
}

虽然并行迭代可能很常见,但似乎不太可能会被大量使用,以至于串行迭代变得多余。似乎最有可能的是人们会希望在项目中混合使用这两种方法,我们应该设法让并行和异步迭代都变得方便。

编译器提示(COMPILER HINTS)

目前为止,本文的大部分内容都在讨论为什么并行语义不是异步迭代的正确默认选项。但是,这并不意味着打算去解决的问题不值得解决。并行语义的默认选项只是一个工具。

如果我们能够使用不同的工具呢?例如,有一个问题是:人们可能会忘记去并行化特定的 streams,损失性能。我们能通过一个提示来解决这个问题么?我认为我们大体上可以。

例如 clippy 可以检测已知的核心 API 实例,这些实例可能应该是并行化的,当它们不是并行化时,就会发出警告。例如处理 TCP 连接,或者处理目录中的文件,都是你可能想要并行化的事情。

或者,如果我们想要任何人把它添加到他们的 API 上:Rust 能够引入一个#[must_par_spawn]提示,该提示可以被添加到结构体上,类似于#[must_use]。 它可以附带一个对应的标签,将一个函数标记为 "这个会产生任务"。如果没有产生任务,编译器可以发出警告。

#[must_par_spawn]
struct HttpStream;

#[par_spawn]
async fn spawn<T, F: Future<Output = T> + 'static>(f: F) -> T;

这两种解决方案在实现之前都需要努力;但它们提出了可以探索的替代方向,而不是诉诸并行异步迭代语法。

工具辅助优化(TOOL-ASSISTED OPTIMIZATION)

到目前为止,在这篇文章中我们已经谈到了为什么异步迭代不应该默认为并行语义。但我确实同意,如果我们有一些并行迭代的语法,那就太好了。在我的并行parallel-stream[3] 文章的 "future directions "部分,我阐述了这可能是什么样子的(使用par for)。

let mut listener = TcpListener::bind("127.0.0.1:8080").await?;
par for stream.await? in listener.incoming() {
    io::copy(&stream, &stream).await?;
}

par for中,循环关键字比如breakcontinue将是无效的,并且每个任务都会在一个任务上生成。这个语法可以在不同的执行策略的支持下,同时适用于同步和非同步的 Rust。最后,能够将并行性作为一个头等的语言概念来讨论,将使更好的诊断、文档和工具得以实现。

例如,无意中在一个贴有#[must_par_spawn]标签的结构体上,把par for x.await写成了for x.await将会得到下面的诊断:

warning: loop is sequential
 --> src/main.rs:2:9
  |
2 |     for stream.await? in listener.incoming() {
  |     ^ help: use `par for stream.await? in listener.incoming()` to parallelize
  |
  = note: `#[warn(sequential_loops)]` on by default

我认为某种形式的并行迭代语法将是极好的。我希望看到它以类似的方式存在于异步和非异步 Rust 中。

附录:LENDING STREAMS(ADDENDUM: LENDING STREAMS)

Scottmcm[4]指出,在 Zulip 上,并行迭代语法无法适用于 lending streams (也被称为“streaming”或者“attached” streams)。

在一个 lending stream 中,获得Stream返回结果的Item可能是借用自自身。它只能在当自身引用活着的时候才能被使用。例如,一个Stream,其中:

type Item<'iter> = &'iter mut State;

将会编译失败,这似乎颇有局限性。

总结(CONCLUSION)

Stream 的并行迭代语义是一个非常重要的概念,具有很大的潜力,但似乎不太可能适合所有情况。

通过为 Stream 默认串行迭代的语义, Iterator 的对等性也被保留下来。这不仅保证了 break 和 continue 等关键词以相同的方式工作,还可以把 "并行迭代 "的设计看成是异步和非异步 Rust 之间可以共享的设计。

在为 Rust 添加异步迭代语法之前,我们还有不少工作要做。stream RFC 草案仍然需要提交和被接受。还有关于在生态系统与 stream 适配器的迁移和互操作的问题。还有一个涵盖从IntoStream到语法的设计。虽然这一切可能还需要一段时间;希望这篇文章可以作为讨论在 Stream 上的迭代应该默认为串行还是并行的锚点。

本文首发于个人公众号: Rust碎碎念 。非授权禁止转载,欢迎扫码关注获取最新文章信息。

参考资料

[1]

transform stream: https://nodejs.org/api/stream.html#stream_class_stream_transform

[2]

csv-parser package: https://github.com/mafintosh/csv-parser

[3]

parallel-stream: https://blog.yoshuawuyts.com/parallel-stream/#future-directions

[4]

Scottmcm: https://github.com/scottmcm