西安做营销型网站建设,服务专业公司网站建设服务,东莞网站推广公司黄页,邯郸做网站推广多少钱博客主要是参考 Asynchronous Programming in Rust #xff0c;会结合简单的例子#xff0c;对 async 和 await 做比较系统的理解#xff0c;如何使用 async 和 await 是本节的重点。 async 和 await 主要用来写异步代码#xff0c;async 声明的代码块实现了 Future 特性会结合简单的例子对 async 和 await 做比较系统的理解如何使用 async 和 await 是本节的重点。 async 和 await 主要用来写异步代码async 声明的代码块实现了 Future 特性如果实现 Future 的代码发生阻塞会让出当前线程的控制权允许线程去执行别的 Future 代码。 我把 async 代码块使用 Future 来表示关键是理解 future 和执行线程之间的关系。系统中存在多个 future时多个 future 的执行顺序如何控制以及多个 future 如何同时并发执行文章会尝试解释这些问题。 多个 async 串行执行 在 Cargo.toml 中增加如下依赖。 [dependencies]
futures 0.3.28使用 async fn 创建异步方法关键字 async 还可以用来声明代码块在闭包函数中可能会见到。
async fn do_something() { /* ... */ }我对原始内容的例子做了简化来探究异步执行的过程。仍然声明了三个 async 方法分别对应「学习唱歌」、「唱歌」、「跳舞」三个函数假设这三个函数可以并行异步执行那么这三个过程的顺序便是随机的、不确定的。
use futures::executor::block_on;async fn learn_song() { println!(learn song);
}async fn sing_song() { println!(sing song);
}async fn dance() {println!(dance);
}async fn async_main() {let f1 learn_song();let f2 sing_song();let f3 dance();futures::join!(f1, f2, f3);
}fn main() {block_on(async_main());
}本地执行的结果是上述代码无论执行多少次输出都是确定的。理论上确实不应该我都有点怀疑会不会代码太简单导致的。
修改一下 learn_song 函数在打印之前执行多次 for 循环。如果这几个过程是并发的那么 learn_song 肯定是最后被执行完成的。遗憾的是控制台第一个输出的还是 “learn song”。这也说明上述代码并没有被并发执行。
async fn learn_song() { let mut i 0;while i 10000 {i i 1;}println!(learn song);
}原文代码示例
问题究竟出在哪里了呢重新看一下原文章的demo试着对比一下哪里出了问题。
async fn learn_and_sing() {// Wait until the song has been learned before singing it.// We use .await here rather than block_on to prevent blocking the// thread, which makes it possible to dance at the same time.let song learn_song().await;sing_song(song).await;
}async fn async_main() {let f1 learn_and_sing();let f2 dance();// join! is like .await but can wait for multiple futures concurrently.// If were temporarily blocked in the learn_and_sing future, the dance// future will take over the current thread. If dance becomes blocked,// learn_and_sing can take back over. If both futures are blocked, then// async_main is blocked and will yield to the executor.futures::join!(f1, f2);
}fn main() {block_on(async_main());
}join!和 await 类似只是join!能够等待多个并发执行的 future如果代码被临时阻塞在 learn_and_singdance 会被当前线程接管。如果 dance 被阻塞learn_and_sing 会重新被接管。如果两个方法同时被阻塞async_main 就会被阻塞会使当前执行阻塞。 通过代码注释我抠到一个字眼 the current thread这难道说明函数 f1 和 f2 是在一个线程上执行的如果它们是在一个线程上执行的又因为方法体内部都没有类似网络IO的阻塞那确实有可能导致串行执行的效果。
rust 对 async 的支持
重新翻看语言和库的支持 Language and library support介绍我决定把中心放在 async 的设计实现上async 在底层是如何被处理的。 最基础的特性、类型和方法比如 Future 特性都被标准库提供async/await 语法直接被 Rust 编译器支持futures 库提供了一些工具类型、宏、函数它们可以被使用在 Rust 应用程序中async runtimes 比如 Takio、async-std提供了执行一部代码、IO、创建任务的能力。大部分的异步应用和异步库都依赖具体的运行时可以查看 The Async Ecosystem 了解细节 关键点在异步运行时上原来 rust 只提供了一套异步运行时的接入标准具体的实现则是由第三方库自己决定的比较有名的的运行时三方库包括 Takio 和 async-std。不得不说Rust 设计的眼界确实比较高。
我想通过 Single Threaded vs Multi-Threaded Executors 来理解单线程和多线程的执行。前面的例子中我们有怀疑代码没有并行是因为在单线程中执行导致的。当然也可能是因为我们没有明确指定 async runtimes。 异步的 executor 可以是单线程、也可以是多线程执行async-executor 库就同时提供了这两种 executor . 多线程 executor 可以同时处理多个任务这会大大加快了处理速度。但任务之间的数据同步成本也会更高一些。建议通过性能测试来决策选择单线程还是多线程 . 任务既可以被创建它的线程执行也可以被其他独立线程执行。即使任务被独立线程执行其运行也是非阻塞的。如果想要调度任务在多线程上执行任务本身必须要实现 Send 特性。有的运行时还支持创建 non-Send 任务用来保证每个任务都被创建它的线程执行。有的库中还支持将生成的阻塞任务交给特定的线程去执行这对于运行阻塞代码会非常有用 看到这里我决定指定 Takio 运行时来重新运行上述代码如何指定呢
指定 Takio 运行时
takio 的官网挺花里胡哨大大的标题Build reliable network applications without compromising speed. 直译过来是构建可靠的网络应用而不向降低运行效率妥协。不过理性提醒我这种既要又要的声明应该得有前提吧。
takio 中的例子是 redis 的写入和读取过程我要介绍的内容都基于 Hello Tokio 示例的解释只不过解释的顺序上会有一点点调整。
如何开启异步运行时 async fn 必须被异步运行时执行运行时包含异步任务调度、IO事件、计时器等。但这个运行时并不会自动开启我们可以通过 main 函数来开启它。 . #[tokio::main] 宏可以将 async fn main() 转换为同步的 fn main()并为它初始化了运行时实例然后执行异步的 async main 函数 在我的例子中main 函数最后也调用了 block_on 函数这里调用的是 rt.block_on 方法不晓得两者是否存在差异不过后文有必要等会去看看在没有指定运行时的情况下 block_on 的执行效果。
Cargo features
引入 tokio 依赖其中的 features 属性可以指定引入的范围。现在我们使用 full 来标识导入所有特性。不过我感觉引入 tokio 会和例子中的 futures 发生冲突引入 tokio 后明显就不需要引入 futures::executor::block_on 了。
还是得继续看看测试效果用上面的例子来验证一下
[dependencies]
tokio { version 1, features [full] }异步程序 对于同步程序来说如果程序执行过程中遇到阻塞操作它就会阻塞在那里直到程序处理完成。拿建立 TCP 连接来说需要建连的两端通过网络进行数据交互但这个过程会花费相当多的时间线程就只能阻塞直到建连完成。 . 对于异步程序来说不能立即完成的操作会被系统挂起到后台当前线程并不会被阻塞它仍可以继续执行别的任务。一旦被挂起的任务恢复它便可以从上次执行的位置重新开始执行。因为我们的例子中只有一个任务因此当操作被挂起时并没有触发别的任务执行但异步程序通常是会包含多个任务的。 关于异步的解释这里特意提到了任务task。只有在多个 task 的情况下异步执行才有意义。上文还一直提到的 executor、recator它们都算得上异步运行时的关键模块。
我忍不住思考最初例子中的「学习唱歌」、「唱歌」、「跳舞」是属于三个独立的任务吧应该是的。
Compile-time green-threading
green-threading 阅读到这里的小伙伴欢迎留言来解释它的具体含义。点击链接可以直接跳转到对应的原文。
异步回归
首先引入 tokio 的包其次使用 #[tokio::main] 来标识开启异步运行时删除之前的 main 函数简单替换成下面的 main 函数声明。非常遗憾下面的代码仍然无法异步执行。
#[tokio::main]
async fn main() {let f1 learn_song();let f2 sing_song();let f3 dance();futures::join!(f1, f2, f3);
}难不成是因为 task 我们现在看看 tokio 中对 task 的用法现在的代码只是有了 tokio 的面并没有 tokio 的里。
task tokio task 是一个异步的 green 线程需要通过 tokio::spawn 方法进行创建该方法返回 JoinHandle 类型对象调用者需要通过这个返回值来和该 task 进行交互。如果 task 包含返回值调用者可以链式对 JoinHandle 调用 await来获取。 结合下面的例子看起来 .await 是用来阻塞等待 task 执行完成的。启动一个 task 之后必须确保 task 执行完成才能继续执行后续的依赖流程有点类似 java 的异步线程。
#[tokio::main]
async fn main() {let handle tokio::spawn(async {// Do some async workreturn value});// Do some other worklet out handle.await.unwrap();println!(GOT {}, out);
}在 JoinHandle 上调用 await 会返回一个 Result 对象。如果 task 在执行期间发生错误JoinHandle 会返回一个 Err 类型。当 task 发生 panic或者被运行时强制取消时便会触发这个 Err。 task 是运行时调度的基本执行单元task 创建后会被提交给 tokio 调度器我们只需要在使用的地方等待 task 执行完成即可。task 可能会被创建它的线程执行也可能会交给别的运行时线程执行总之可以在不同的线程上被执行。 在 tokio 中 task 是非常轻量的它们只需要单个分配和64字节的内存。 和 go 语言比较的话task 相当于 GMP 调度中的 G 对象也是一个非常轻量的对象可以在不同的线程中被调用。
使用 tokio 来重构
上面也提到过现在的代码只有 tokio 的表没有 tokio 的里现在我们重新使用 tokio 异步来实现一遍。通过简单的改造终于实现了异步执行的效果不禁感慨确实不易啊。
fn learn_song() { println!(learn song);
}fn sing_song() { println!(sing song);
}async fn dance() {println!(dance);
}#[tokio::main]
async fn main() {let f1 tokio::spawn(async {learn_song();});let f2 tokio::spawn(async {sing_song();});let f3 tokio::spawn( dance());futures::join!(f1, f2, f3);
}回顾
rust 异步调用的运行时是依赖三方实现的在不引入 tokio 的情况下并不会实现并发。同时rust 中 tokio 的设计也类似于 go 语言中 GMP 的设计异曲同工。