Rust 学习笔记:future、任务和线程

Rust 学习笔记:future、任务和线程

Rust 学习笔记:future、任务和线程

线程提供了一种实现并发的方法。对 future 和 Stream 使用 async 是另一种实现并发的方法。

这两种方法不是二选一。在许多情况下,选择不是线程还是异步,而是线程和异步。

许多操作系统已经提供了基于线程的并发模型,因此许多编程语言也支持它们。然而,这些模型并非没有权衡。在许多操作系统上,它们为每个线程使用相当多的内存,并且它们在启动和关闭时带来了一些开销。当操作系统和硬件支持线程时,线程也是一种选择。与主流桌面和移动计算机不同,一些嵌入式系统根本没有操作系统,因此它们也没有线程。

异步模型提供了一组不同的(最终是互补的)权衡。在异步模型中,并发操作不需要它们自己的线程。相反,它们可以在任务上运行。任务类似于线程,但不是由操作系统管理,而是由异步运行时管理。

我们可以通过使用异步通道构建流,并生成可以从同步代码调用的异步任务。

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let mut count = 0;
        loop {
            trpl::sleep(Duration::from_millis(1)).await;
            count += 1;

            if let Err(send_error) = tx.send(count) {
                eprintln!("Could not send interval {count}: {send_error}");
                break;
            };
        }
    });

    ReceiverStream::new(rx)
}

我们可以用线程做同样的事情。

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    // This is *not* `trpl::spawn` but `std::thread::spawn`!
    thread::spawn(move || {
        let mut count = 0;
        loop {
            // Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`!
            thread::sleep(Duration::from_millis(1));
            count += 1;

            if let Err(send_error) = tx.send(count) {
                eprintln!("Could not send interval {count}: {send_error}");
                break;
            };
        }
    });

    ReceiverStream::new(rx)
}

前者在运行时生成异步任务,后者生成操作系统线程,结果流也不会受到差异的影响。

尽管它们有相似之处,但这两种方法的行为非常不同,尽管在这个非常简单的示例中我们可能很难衡量它。我们可以在任何一台 PC 上生成数百万个异步任务。如果我们试图用线程来做这件事,我们就会耗尽内存!

然而,这些 API 如此相似是有原因的。线程充当同步操作集合的边界,线程之间可以并发。任务充当异步操作集合的边界,并发在任务之间和任务内部都是可能的,因为任务可以在其主体中的 future 之间切换。

future 是 Rust 并发的最小单位。每个 future 可能是其他 future 的树形结构。

运行时(特别是它的执行器)管理任务,而任务管理 future。在这方面,任务类似于轻量级的、由运行时管理的线程,只是增加了由运行时而不是操作系统管理的功能。

这并不意味着异步任务总是比线程好(反之亦然)。线程并发在某种程度上是比异步并发更简单的编程模型。这可能是优点,也可能是缺点。线程有点像“一次启动就进行到底”,它没有像 future 那样的原生机制,因此线程会一直运行,除非被操作系统打断。线程不像 future 那样内置对任务内并发性的支持。线程也没有取消机制——这是我们没有明确讨论的主题,但是当我们结束一个 future 时,它的状态会被正确地清理,这一事实暗示了这一点。

因此,任务给了我们对未来的 future 控制,允许我们选择何时以及如何将它们分组。事实证明,线程和任务通常可以很好地协同工作,因为任务可以(至少在某些运行时)在线程之间移动。事实上,在底层,我们一直在使用的运行时——包括 spawn_blocking 和 spawn_task 函数——默认是多线程的!

许多运行时使用一种称为“工作窃取”的方法,根据线程当前的使用情况,在线程之间透明地移动任务,以提高系统的整体性能。这种方法实际上需要线程和任务,因此也需要 future。

在考虑何时使用哪种方法时,请考虑以下经验法则:

  • 如果工作是高度并行的,例如处理一堆数据,其中每个部分都可以单独处理,那么线程是更好的选择。

  • 如果工作是高度并发的,例如处理来自不同来源的消息,这些消息可能以不同的间隔或不同的速率传入,那么异步是更好的选择。

如果同时需要并行性和并发性,则不必在线程和异步之间进行选择。你可以自由地使用它们,让每一个都发挥它最擅长的作用。

例如:

use std::{thread, time::Duration};

fn main() {
    let (tx, mut rx) = trpl::channel();

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::run(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}

我们首先创建一个异步通道,然后生成一个线程,该线程获得通道的发送端所有权。在线程中,我们发送数字 1 到 10,在每个数字之间休眠 1 s。最后,我们运行通过传递给 trpl::run 的异步块创建的 future。在 future 中,我们等待这些消息,接收并打印出来。

想象一下使用专用线程运行一组视频编码任务(因为视频编码是计算绑定的),但通知 UI 这些操作是通过异步通道完成的。在真实的用例中有无数这样的组合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值