Hello Tokio
我们从写一个最基础的的 Tokio 程序开始,这个程序会连接到 MiniRedis 的服务端,然后设置一个 key 为 hello,value 为 world 的键值对,然后再把这个键值对读取回来。这些操作我们会使用名为 Mini-Redis 的客户端库来完成。
The Code
创建一个新程序 Generate a new crate
我们从创建一个新的 Rust 程序开始
cargo new my-redis
cd my-redis
添加依赖 Add dependencies
接下来,打开 Cargo.toml ,并在 [dependencies] 后添加下面的代码
tokio = { version = "1", features = ["full"] }
mini-redis = "0.4"
开始写代码 Write the code
然后,打开 main.rs 并用下面的代码替换文件的内容
use mini_redis::{client, Result}; #[tokil::main] pub async fn main() -> Result<()> { // Open a connection to the mini-redis address. let mut client = client::connect("127.0.0.1:6379").await?; // Set the key "hello" with value "world" client.set("hello", "world".into()).await?; // Get key "hello" let result = client.get("hello").await?; println!("got value from the server; result={:?}", result); Ok(()) }
为了确保 Mini-Redis 的服务端处于运行状态,我们打开一个终端窗口,运行如下命令:
mini-redis-server
接下来运行我们的 mini-redis 程序
$ cargo run
got value from the server; result=Some(b"world)
成功了!
你可以从 这里 找到完整的源码.
Break it down
接下来花点时间梳理下我们刚才做的事情。代码并不多,但其中却触发了许多的事情。
#![allow(unused)] fn main() { let mut client = client::connect("127.0.0.1:6379").await?; }
函数 client::connect 是 mini-redis 这个包所提供的,他会使用指定的地址来异步的创建一个 TCP 连接,当这个连接建立成功时, client 则保存了该函数返回的结果。尽管这个操作是异步发生的,但代码 看起来 却是同步的。其中唯一指示了该操作为异步的只有 .await 操作符。
什么是异步编程 What is asynchronous programming?
大部分的电脑程序都按照他们代码所写的顺序执行,最前面的先执行,然后是下一行,然后一直执行下去。在同步编程中,当程序遇到了一个无法立即完成的操作时,他会堵塞在该位置一直到操作完成,举个例子,在创建 TCP 连接时连接双方需要在网络中交换一些信息,交换信息的操作需要花费相当的时间,而运行这段代码的线程在这个时间内将被阻塞。
在异步编程中,如果一个操作不能马上完成的话,他将被暂停然后切换到后台等待,执行的线程不会被阻塞,因此他可以继续执行其他的事情。当这个操作完成时,他又会被切换至前台并从之前中断的地方继续执行。我们刚刚实现的示例只启动了一个任务,所以在这个任务的操作被暂停时并没有发生任何其他的事情,但通常异步的编程会同时运行许多的任务。
尽管异步编程能够给我们带来更快的程序,与此同时他也为程序带来了更高的复杂度。开发人员为了能够在异步操作完成时将任务重新恢复执行,需要去跟进任务的运行状态。从历史经验上来看,这是一个乏味并且非常容易出错的工作。
编译时的绿色线程 Compile-time green-threading
Rust 使用了 async/await 特性来实现了异步编程的功能。会执行异步操作的函数通过 async 关键字进行标识,在我们的示例中, connect 函数进行了如下的定义:
#![allow(unused)] fn main() { use mini_redis::Result; use mini_redis::client:Client; use tokio::net::ToSocketAddrs; pub async fn connect<T: ToSocketAddrs>(addr: T) -> Result<Client> { // ... } }
async fn 的定义跟同步函数很类似,但他以异步的方式执行。Rust 在编译时将代码转换为异步的操作,所有使用 .await 调用并定义为 async fn 的操作将让出线程的执行权。这样该线程就能在异步操作被放到后台的期间做其他的事情。
尽管也有一些其他的编程语言实现
async/await的特性,但Rust使用了一个独立的方式,最主要的一点是Rust的异步操作是lazy的。这导致了运行时的语义跟其他编程语言的产生了区别。
如果到现在还没弄得很明白,不用担心,我们还会继续在接下来的篇幅中探讨 async/await。
使用 async/await Using async/await
异步函数能够与普通的 Rust 函数一样使用。但是,调用这些函数不意味着执行这些函数,调用 async fn 类型的函数返回的是一个代表该操作的标识。在概念上他跟一个无参的闭包函数类型。为了能够真正的执行它,你需要在函数返回的标识上使用 .await 操作。
我们来看看下面的例子
async fn say_world() { println!("world"); } #[tokio::main] async fn main() { // Calling `say_world()` does not execute the body of `say_world()` let op = say_hello(); // This println! comes first println!("hello"); // Calling `.await` on `op` starts executing `say_world`. op.await; }
输出
hello
world
async fn 函数的返回结果是一个实现了 Future trait 的匿名类型。
所以这里到底是怎么执行的,还得看
Rust最终转换出的代码及Future的定义,后续我会单独细讲
异步的 main 函数 Async main function
用来启动程序的 main 函数其他普通的 Rust 程序的有所不同:
- 被定义为
async fn - 添加了
#[tokio::main]宏
async fn 函数在我们需要执行异步操作的上下文中被使用。然而,异步函数需要通过 runtime 来运行,runtime 中包含异步任务的调度器,他提供了事件驱动的 I/O、定时器等。runtime 并不会自动的运行,所以需要在主函数中运行它。
我们在 async fn main() 函数中添加的 #[tokio::main] 宏会将其转换为同步的 fn main() 函数,该函数会初始化 runtime 并执行我们定义的异步的 main 函数。
比如
#[tokio::main] async fn main() { println!("hello"); }
会被转换为
fn main() { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("hello"); }) }
Tokio 中具体的 runtime 的细节在后续的章节中会补充。
Cargo 特性 Cargo features
我们在定义对 Tokio 的依赖时使用 full 特性。
tokio = { version = "1", features = ["full"] }
Tokio 提供了大量的功能 (TCP, UDP, Unix sockets, Timers, sync utilities, multiple scheduler types 等),但并不是所有的程序都需要用到这么多的功能。在需要缩短编译时间或减小程序大小时,可以只选择所需的特性。
现在的话,还是继续使用 full 特性吧。