为什么使用异步?

我们都喜欢Rust让我们能够编写快速且安全的软件的方式,但为什么写异步代码呢?

异步编程,或者叫异步,是一种被越来越多编程语言支持的并发编程模型。它能够在一小撮 OS 线程上运行一大堆并发任务,同时还能通过 async/await 语法,保持原本同步编程的观感。

异步 vs 其他并发模型

并发编程相对于常规、顺序式编程不够成熟或“标准化”。结果是,我们表达并发的方式不一样,取决于语言支持哪种并发模型。简短地介绍最流行的并发模型能帮助你理解异步编程是如何适合更广阔的并发编程领域:

  • OS 线程 不需要编程模型作任何改动,这使得表达并发很容易。然而,线程间同步可能会很困难,并且性能开销很大。线程池可以减少一部分开销,但是不足够支持超大量 IO 密集负载。
  • 事件驱动编程,以及 回调,可以变得高性能,但倾向于导致冗长,“非线性”的控制流。数据流和错误传播通常就变得很难跟进了。
  • 协程,就像线程,但不需要改变编程模型,于是他们变得便于使用。像异步,他们可以支持大量的任务。然而,他们抽象了对于系统编程和自定义运行时实现非常重要的底层细节。
  • actor 模型 把所有的并发计算分割成称为 actor 的单元,相互之间通过易错的消息传递进行沟通,非常类似于分布式系统。actor 模型能够很高效地实现,但是它还很多没有解答的实践问题,例如流程控制和重入逻辑。

总之,异步编程既允许非常适合像Rust的低层语言的高效实现,同时也提供了线程和协程的大部分工效学效益。

Rust 的异步 vs 其他语言的

尽管很多语言都支持异步编程,但实现细节上有很多不一样。Rust 的异步实现和大部分语言的在以下方面有区别:

  • Rust 中 Futures 是惰性的,并且只有被轮询才会进一步执行。丢弃(Dropping)一个 future 可以阻止它继续执行。
  • Rust 中的 异步是零成本的,这意味着你只需要为你所使用的东西付出代价。特别来说,你使用异步时可以不需要堆分配或动态分发,这对性能来说是好事!这也使得你能够在约束环境下使用异步,例如嵌入式系统。
  • Rust 不提供内置运行时。相反,运行时由社区维护的库提供。
  • Rust里 单线程的和多线程的 运行时都可用,而他们会有不同的优劣。

Rust 中的异步 vs 线程

Rust 中异步的首选替代是使用 OS 线程,可以直接通过 std::thread 或者间接通过线程池来使用。从线程模型迁移到异步模型,或者反过来,通常需要一系列重构的工作,既包括内部实现也包括任何暴露的公开接口(如果你在构建一个库)。因此,尽早地选择适合你需要的模型能够节约大量的开发事件。

OS 线程 适合少量任务,因为线程会有 CPU 和内存开销。生成和切换线程是代价相当昂贵,甚至闲置的线程也会消耗系统资源。一个线程池库可以减轻这些开销,但并不能全部健康。然而,线程能让你重新利用存在的同步代码,而不需要大改源代码——不需要特别的编程模型。一些操作系统中,你也可以改变线程的优先级,这对于驱动或者其他延迟敏感的应用很有用。

异步 极大地降低了 CPU 和内存开销,尤其是再负载大量越过IO 边界的任务,例如服务器和数据库。同样,你可以处理比 OS 线程更高数量级的任务,因为异步运行时使用少量(昂贵的)线程来处理大量(便宜的)任务。然而,异步 Rust 会导致更大的二进制体积,因为异步函数会生成状态机,并且每个可执行文件都会绑定一个异步运行时。

最后一点,异步编程并没有 更优于 线程模型,不过它们是不一样的。如果你不需要由于性能原因使用异步,线程通常是个更简单的替换。

例子:并发下载

这个例子的目标,是并发地下载两个网页。在典型的线程化(threaded)应用中,我们需要生成线程来达到并发:

fn get_two_sites() {
    // 生成两个线程来下载网页.
    let thread_one = thread::spawn(|| download("https:://www.foo.com"));
    let thread_two = thread::spawn(|| download("https:://www.bar.com"));

    // 等待两个线程运行下载完成.
    thread_one.join().expect("thread one panicked");
    thread_two.join().expect("thread two panicked");
}

然而,下载网页是小任务,为了这么少量工作创建线程相当浪费。对更大的应用来说,这很容易就会变成瓶颈。在异步 Rust,我们能够并发地运行这些任务而不需要额外的线程:

async fn get_two_sites_async() {
    // 创建两个不同的 "futures", 当创建完成之后将异步下载网页.
    let future_one = download_async("https:://www.foo.com");
    let future_two = download_async("https:://www.bar.com");

    // 同时运行两个 "futures" 直到完成.
    join!(future_one, future_two);
}

这里没有创建额外的线程。此外,所有函数调用都是静态分发的,也没有堆分配!然而,我们需要先编写能够异步执行的代码,而这本书会帮助你做到。

Rust 中的自定义并发模型

最后一点, Rust 不会强制你从线程模型和异步模型中间只选一个。你可以在同一个应用里同时使用两个模型,这在你混合了线程化的和异步的依赖时非常有用。事实上,你甚至可以同时使用不同的并发模型,例如事件驱动编程,只要你能找到一个实现它的库。