Rust async 编程

Posted 小乔的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Rust async 编程相关的知识,希望对你有一定的参考价值。

Rust async 编程

Asynchronous Programming in Rust:https://rust-lang.github.io/async-book/

中文书名《Rust 异步编程指南》:https://github.com/rustlang-cn/async-book

Rust语言圣经(Rust Course):https://course.rs/advance/async/getting-started.html

一、Getting Started

1.1 为什么使用 async

为什么使用 async

  • Async 编程,是一种并发(concurrent)编程模型
    • 允许你在少数系统线程上运行大量的并发任务
    • 通过 async/await 语法,看起来和同步编程差不多

其它的并发模型

  • OS 线程
    • 无需改变任何编程模型,线程间同步困难,性能开销大
    • 线程池可以降低一些成本,但难以支撑大量 IO 绑定的工作
  • Event-driven 编程
    • 与回调函数一起用,可能高效
    • 非线性的控制流,数据流和错误传播难以追踪
  • 协程(Coroutines)
    • 类似线程,无需改变编程模型
    • 类似 async ,支持大量任务
    • 抽象掉了底层细节(这对系统编程、自定义运行时的实现很重要)
  • Actor 模型
    • 将所有并发计算划分为 actor , 消息通信易出错
    • 可以有效的实现 actor 模型,但许多实际问题没解决(例如流程控制、重试逻辑)

Rust 中的 async

  • Future 是惰性的
    • 只有poll时才能取得进展, 被丢弃的 future 就无法取得进展了
  • Async是零成本的
    • 使用async ,可以无需堆内存分配(heap allocation)和动态调度(dynamic dispatch),对性能大好,且允许在受限环境使用 async
  • 不提供内置运行时
    • 运行时由Rust 社区提供,例如 tokio
  • 单线程、多线程均支持
    • 这两者拥有各自的优缺点

Rust 中的 async 和线程(thread)

  • OS 线程:
    • 适用于少量任务,有内存和CPU开销,且线程生成和线程间切换非常昂贵
    • 线程池可以降低一些成本
    • 允许重用同步代码,代码无需大改,无需特定编程模型
    • 有些系统支持修改线程优先级
  • Async:
    • 显著降低内存和CPU开销
    • 同等条件下,支持比线程多几个数量级的任务(少数线程支撑大量任务)
    • 可执行文件大(需要生成状态机,每个可执行文件捆绑一个异步运行时)

Async 并不是比线程好,只是不同而已!

总结:

  • 有大量 IO 任务需要并发运行时,选 async 模型
  • 有部分 IO 任务需要并发运行时,选多线程,如果想要降低线程创建和销毁的开销,可以使用线程池
  • 有大量 CPU 密集任务需要并行运行时,例如并行计算,选多线程模型,且让线程数等于或者稍大于 CPU 核心数
  • 无所谓时,统一选多线程

例子

如果想并发的下载文件,你可以使用多线程如下实现:

fn get_two_sites() 
    // Spawn two threads to do work. 创建两个新线程执行任务
    let thread_one = thread::spawn(|| download("https://www.foo.com"));
    let thread_two = thread::spawn(|| download("https://www.bar.com"));

    // Wait for both threads to complete. 等待两个线程的完成
    thread_one.join().expect("thread one panicked");
    thread_two.join().expect("thread two panicked");

使用async的方式:

async fn get_two_sites_async() 
    // Create two different "futures" which, when run to completion, 创建两个不同的`future`,你可以把`future`理解为未来某个时刻会被执行的计划任务
    // will asynchronously download the webpages. 当两个`future`被同时执行后,它们将并发的去下载目标页面
    let future_one = download_async("https://www.foo.com");
    let future_two = download_async("https://www.bar.com");

    // Run both futures to completion at the same time. 同时运行两个`future`,直至完成
    join!(future_one, future_two);

自定义并发模型

  • 除了线程和async,还可以用其它的并发模型(例如 event-driven)

1.2 Rust Async 的目前状态

Async Rust 目前的状态

  • 部分稳定,部分仍在变化。
  • 特点:
    • 针对典型并发任务,性能出色
    • 与高级语言特性频繁交互(生命周期、pinning)
    • 同步和异步代码间、不同运行时的异步代码间存在兼容性约束
    • 由于不断进化,维护负担更重

语言和库的支持

  • 虽然Rust本身就支持Async编程,但很多应用依赖与社区的库:
    • 标准库提供了最基本的特性、类型和功能,例如 Future trait
    • async/await 语法直接被Rust编译器支持
    • futures crate 提供了许多实用类型、宏和函数。它们可以用于任何异步应用程序。
    • 异步代码、IO 和任务生成的执行由 "async runtimes" 提供,例如 Tokio 和 async-std。大多数async 应用程序和一些 async crate 都依赖于特定的运行时。

注意

  • Rust 不允许你在 trait 里声明 async 函数

编译和调试

  • 编译错误:
    • 由于 async 通常依赖于更复杂的语言功能,例如生命周期和Pinning,因此可能会更频繁地遇到这些类型的错误。
  • 运行时错误:
    • 每当运行时遇到异步函数,编译器会在后台生成一个状态机,Stack traces 里有其明细,以及运行时调用的函数。因此解释起来更复杂。
  • 新的失效模式:
    • 可能出现一些新的故障,它们可以通过编译,甚至单元测试。

兼容性考虑

  • async和同步代码不能总是自由组合

    • 例如,不能直接从同步函数来调用 async 异步函数
  • Async 代码间也不总是能自由组合

    • 一些crate依赖于特定的 async 运行时
  • 因此,尽早研究确定使用哪个 async 运行时

性能特征

  • async 的性能依赖于运行时的表现(通常较出色)

1.3 async/await 入门

async

  • async 把一段代码转化为一个实现了Future trait 的状态机
  • 虽然在同步方法中调用阻塞函数会阻塞整个线程,但阻塞的Future将放弃对线程的控制,从而允许其它Future来运行。
~/rust via 

Tokio 1.0发布,Rust异步编程框架

Tokio 1.0 稳定版本已发布,Tokio 是 Rust 的异步 runtime,可用于编写快速、可靠的网络应用。Tokio 还提供用于 TCP、UDP、计时器、多线程、工作窃取算法(work-stealing)调度等的 API。

Tokio 团队表示,虽然 Tokio 从四年前刚诞生起就一直在不断发展,不过出现真正的显著变化是在一年前,因为 Rust 在当时才正式支持 async/await。

示例代码

使用 Tokio 编写的基本 TCP echo 服务器:

use tokio::net::TcpListener;use tokio::io::{AsyncReadExt, AsyncWriteExt};#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {    let mut listener = TcpListener::bind("127.0.0.1:8080").await?;    loop {        let (mut socket, _) = listener.accept().await?;

       tokio::spawn(async move {            let mut buf = [0; 1024];            // In a loop, read data from the socket and write the data back.
           loop {                let n = match socket.read(&mut buf).await {                    // socket closed
                   Ok(n) if n == 0 => return,                    Ok(n) => n,                    Err(e) => {
                       eprintln!("failed to read from socket; err = {:?}", e);                        return;
                   }
               };                // Write the data back
               if let Err(e) = socket.write_all(&buf[0..n]).await {
                   eprintln!("failed to write to socket; err = {:?}", e);                    return;
               }
           }
       });
   }
}

在发布公告中,Tokio 团队还介绍了 Tokio 的知名用户和案例。例如,Discord 通过使用 Tokio 将长尾延迟 (Tail Latency) 降低了 5 倍;Fly.io 使用 Tokio 后轻易满足了性能要求;AWS 的 Lambda 团队也使用 Tokio 实现了更可靠、更灵活的服务。

1.0 作为重要版本更新,Tokio 团队对其稳定性做出了保证,承诺会为 Rust 生态提供稳定的基础设施。Tokio 团队表示目前没有关于 Tokio 2.0 的计划,并承诺至少在 3 年内不发布 Tokio 2.0。他们计划为 1.0 提供至少 5 年的维护支持。

当然,稳定并不意味着 Tokio 停滞不前,Tokio 团队介绍了他们未来要完成的工作:推动 Stream trait 进入 Rust 标准库、实现 io_uring 接口、集成 tracing 以及完善 Tokio 的整体生态。这里的生态就是 Tokio 团队所说的 Tokio stack。举个例子,Tokio 为标准基元 (standard primitives),如 socket 和定时器提供了 runtime 和异步API,但网络应用通常会使用更高级别的协议,如 HTTP 和 gRPC。因此 Tokio stack 会提供 HTTP 的 Hyper 和 gRPC 的 Tonic 以满足需求。

最后,Tokio 团队表示随着 Tokio 的推出,他们会专注于开发 Tower,这是一套用于构建可靠客户端和服务器的可重用组件。

详情查看 https://tokio.rs/blog/2020-12-tokio-1-0


福利

10本技术书籍任你选,倒计时最后1天

Tokio 1.0发布,Rust异步编程框架

活动详情:


Tokio 1.0发布,Rust异步编程框架




觉得不错,请点个在看

以上是关于Rust async 编程的主要内容,如果未能解决你的问题,请参考以下文章

Tokio 1.0发布,Rust异步编程框架

Rust网络编程框架-深入理解Tokio中的管道

Rust网络编程框架-深入理解Tokio中的管道

Rust编程语言里的future

Rust futures: async fn 中的 thread::sleep 和阻塞调用

RUST Ex00 Async-std