Why Async?
为什么选择 Rust ?
我们都喜欢 Rust 这种可以让我们去编写高性能且安全的软件的特性。但在异步编程中, 如何同样的保证这一点呢?
异步编程,或简称异步,是一种被越来越多的语言所支持的并发编程模式。它允许你在
少量的 OS 进程上运行大量的并发任务, 通过使用async/await
语法,可同时使其在
使用和感观上基本等同于普通的同步编程。
异步与其它并发模式对比
并发编程并不如常规的同步编程成熟,也没那么标准化。因此,我们需根据语言支持的 并发模型,以不同的方式表达并发。 下面简单介绍最受欢迎的并发模型,这应该可以帮助你去理解异步编程如何适应更广泛的 并发编程领域:
- OS 线程 不需要对编程模型进行任何修改,这使你可非常方便的进行并发编程。 然而,在线程之间进行同步很困难,且带来的性能开销很大。线程池可减少一些开销, 但并不足以满足海量的 I/O 密集工作负载。
- 事件驱动编程(Event-driven programming),与回调结合来使用,可以非常高效, 但往往会导致冗长的,非线性的控制流。数据流和错误信息通常难以追踪。
- 协程,和线程一样,不需要对编程模型进行任何修改,这使得使用它变得非常简单。 同时和异步一样,它也可以支持海量的任务。但是,它抽象出了对系统编程来说很重要 的低级细节与自定义运行时的执行器。
- actor 模型 将所有的并发计算划分成
actor
单元,这使得错误信息的传递变得简单 ,就和分布式系统一样。actor 模型可以有效的实现并发编程,但它留下了许多未解决的 实际问题,如流的控制和重试逻辑。
总之,异步编程可实现高性能计算,且适用于 Rust 这种低级编程语言,它同时提供了线程 和协程中的大部分人性化的优点。
Rust 与其它语言中的异步对比
尽管在许多语言中,都支持进行异步编程,但一些细节因实现而异。Rust 对异步的实现与 大部分编程语言有以下几个不同:
- Rust 中的 Futures 只有在进行轮询时,才会执行,删除 future 会停止其进一步 执行。
- Rust 中 Async 是零开销 的,这意味着只有所执行的任务才会消耗算力,具体来讲, 你没有在 async 过程中进行堆的分配和动态调度,这可使性能得到充分的发挥! 这让你可以在资源有限的环境中使用 async,如嵌入式系统中。
- Rust 并未提供内置运行时环境,而是由社区维护的 crates 提供。
- Rust 中提供了 单线程和多线程 的运行时环境,它们各有不同的优势与缺点。
Rust 中异步和多线程的对比
Rust 中异步的主要替代方法是使用系统进程,或直接使用
std::thread
生成,亦或通过线程池调用。
从线程迁移至异步,通常的主要工作是进行重构,去实现功能和暴露的
APIS(如果你构建一个库),反之亦然。所以,在早期选定适合你需求的模型会大大节省你的
开发时间。
系统线程 适用于少量的任务,因为进程会消耗 CPU 和内存的开销。生成新进程 或是在进程之间切换的代价很高,即使是空闲的进程也在消耗系统资源。诚然,使用进程池 会一定程度上减少这些开销,但不能消除。好处是,使用线程可以让你无需对代码进行大量修 改即可复用——即其不需要特定的编程模型。在一些系统中,你可以定义线程的优先级,这在 如驱动或其它低延时程序中非常有用。
异步 可显著的降低带来的 CPU 和内存的消耗,特别是对于海量 IO 密集型的任务负载, 如服务器和数据库。在其它条件相同的情况下,它可以使你运行比使用系统线程时 多几个数量级的任务,因为异步的运行时环境使用少量(高代价)的线程来处理巨量的(廉价) 的任务。然而,由于异步函数生成了大量的状态机,且每个都可执行块都绑定了一个异步运 行时环境,其生成了更大的二进制 blobs。
最后提醒一下,异步并不比进程好,它们只是实现不同。如果出于性能方面考虑并不一定要 使用异步,那线程通常会是一个更简单的选择。
示例: 并发下载
在这个例子中,我们要实现同时下载两个页面。在典型的线程应用中, 我们需要创建线程来实现并发:
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");
}
然而,下载一个网页是个极小的任务,为之创建一个进程是十分浪费资源的, 在一个更大的程序中,它很容易成为瓶颈。在 Rust 异步中, 我们可以并行的运行这些任务而无需额外进程。
async fn get_two_sites_async() {
// Create two different "futures" which, when run to completion,
// will asynchronously download the webpages.
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.
join!(future_one, future_two);
}
这里,没有创建额外的进程。此外,所有的函数调用都是静态分配的,没有额外的堆分配! 但是,我们首先需要实现异步编程,而此书将帮助你完成它。
Rust 中的自定义并发模型
最后一点要强调的是,Rust 并不强制你在线程和异步之间做出选择。 你可以在一个程序中同时使用这两种模型,有时混合使用线程和异步依赖时会有更好的效果。 实际上,你还可以同时使用其它不同的并发模型,如事件驱动编程,只要你找到一个合适的库来实现它!