#scope #future #async #stable #local #structured #moro

moro-local

为异步 Rust 提供实验性的结构化并发支持(类似于 trio 的 nurseries)。在稳定 Rust 上与不可发送 futures 一起工作。

1 个不稳定版本

新版本 0.4.0 2024 年 8 月 16 日

#255异步

Download history 87/week @ 2024-08-10

87 每月下载次数

MIT/Apache

25KB
276

moro-local

moro 的分支,在稳定 Rust 上运行,专注于本地(非 Send)futures。

TL;DR

类似于 rayonstd::thread::scope,moro 允许您使用 moro::async_scope! 宏创建一个 作用域。在这个作用域内,您可以启动可以访问作用域外定义的堆栈数据的作业

let value = 22;
let result = moro::async_scope!(|scope| {
    let future1 = scope.spawn(async {
        let future2 = scope.spawn(async {
            value // access stack values that outlive scope
        });

        let v = future2.await * 2;
        v
    });

    let v = future1.await * 2;
    v
})
.await;
eprintln!("{result}"); // prints 88

堆栈访问,核心作用域 API

调用 moro::async_scope!(|scope| ...) 并返回整个作用域的 future,您可以使用 await 来启动作用域。

在作用域体 (...) 中,您可以调用 scope.spawn(async { ... }) 来启动一个作业。此作业必须在作用域自身完成之前终止。`scope.spawn` 的结果是作业返回的结果。

提前终止和取消

Moro作用域支持提前终止取消。您可以通过调用scope.terminate(v).await来终止作用域内的所有子线程。终止通常用于当v是一个Result,以取消Err值(在导入模块中我们提供了如unwrap_or_cancel这样的辅助方法)。

使用取消的一个例子可以在monitor中找到——在这个例子中,创建了多个任务,它们都检查输入中的一个整数。如果有任何整数是负数,则整个作用域将被取消。

未来工作:与类似rayon的迭代器集成

我想做这个。 :)

常见问题

moro这个名字是从哪里来的?

它来自希腊语中“婴儿”(μωρό)这个词。流行的"trio"库使用“nursery”来指代作用域,因此我想尊重这一传统。

然而,“moros”也是即将到来的灾难的'hateful'精神,这是我之前不知道的,但感觉有点酷。

是否有其他异步子例程项目可用,moro如何比较?

是的!我知道...

  • async_nursery,它与moro类似,但提供并行执行(而不仅仅是并发),但——作为结果——需要'static约束。
  • FuturesUnordered,它可以作为某种子例程使用,但它也有一些已知的问题。这种类型目前在moro实现中使用,但moro的API阻止了这些问题发生。
  • select操作通常用于“模拟”并行流;与FuturesUnordered一样,这是一个容易出错的途径,而moro部分地是为了作为select-like API的替代品而演化的。

为什么moro产生的任务只并发运行,而不并行运行?

使用当前的Rust,并行运行的moro任务不能安全执行。详细内容将在后面的问题中说明,但简单地说,当moro作用域向其调用者让步时,作用域正在“放弃控制权”,而调用者可以选择——如果它选择的话——完全忘记作用域并停止执行它。这意味着如果moro作用域已经启动了并行线程,这些线程将继续访问调用者的数据,这可能会创建数据竞争。不太好。

并发运行不是有很大的限制吗?

有点吧?并行肯定很好,但对于许多异步服务器来说,您可以在连接之间获得并行性,而不需要在连接内部有并行性。您还可以使用其他机制来获得并行性,但需要'static约束。

好吧,但为什么moro产生的任务只并发运行,而不并行运行?给我详细解释一下!

方法 Future::poll 允许安全代码“部分推进”一个未来,然后,因为未来是一个普通的Rust值,所以可以“忘记”它(例如,通过 std::mem::forget,尽管还有其他方法)。这允许你创建一个作用域,执行几次,然后丢弃它而不运行任何析构函数

async fn method() {
    let data = vec![1, 2, 3];
    let some_future = moro::async_scope!(|scope| {
        scope.spawn(async {
            for d in &data {
                tokio::task::yield_now().await;
            }
        });
    });

    // pseudo-code, we'd have to gin up a context etc:
    std::future::Future::poll(some_future);
    std::future::Future::poll(some_future);
    std::mem::forget(some_future);
    return;
}

如果moro任务并行运行,我们就没有办法确保在 method 返回之前停止作用域内创建的并行线程。结果,即使在栈帧弹出和数据释放后,它们仍然会访问 data 中的数据。不好。

但由于moro仅限于并发,这没关系。作用域内的任务只有在它们被轮询时才会推进(它们不是并行的)——所以当你“忘记”作用域时,你只是停止执行任务。

请注意,这个问题不会出现在像 rayon 或新的 std::thread::scope 调用这样的库中。这是因为同步代码具有异步代码所缺少的能力:同步函数可以阻塞其调用者(这是因为安全的Rust禁止longjmp)。但在异步代码中,在Rust的当前模型下,只要“await”某事,你就是在放弃对调用者的控制,他们可以自由地不再轮询你。这意味着,我相信,不可能有像moro那样的“作用域”,可以安全地引用作用域之外的数据,因为该数据属于你的调用者,你不能强迫他们不返回。换句话说,异步代码可以通过与执行器的合作,确保某些未来运行到完成。任何对 tokio::spawn 的调用都会做到这一点。但你不能确保你的未来是嵌入在某种运行到完成的东西中的。

我相信,没有修改 Future 特征或以某种方式修改Rust,就不能安全地启用并行执行。有许多建议要改变 Future 特征,以允许moro支持并行执行(这些相同的建议将有助于支持io-uring、DMA和其他功能),但前进的具体路径尚未确定。

依赖

~1–1.6MB
~33K SLoC