Tokio第一天 Runtime

485 阅读4分钟

1. Tokio配置

引入Tokio依赖很简单,只需要

[dependencies]
tokio = { version = "1.29.1", features = ["full"] }

在不熟悉的情况下将所有features引入,后续可以只引入需要的features以减小打包后体积,降低编译耗时.

2. 创建异步运行时

之前介绍Rust异步编程就提到过,Rust的标准库中是不包含异步运行时的,因此需要借助第三方的运行时并且在编译的时候打包到生成的可执行文件中.

let rt=tokio::runtime::Runtime::new().unwrap();
let rt=tokio::runtime::Builder::new_multi_thread().worker_threads(10).enable_all().build().unwrap();
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();

上面是三种创建异步运行时的方式,不同点在于上面两种方式创建的运行时默认是多线程的运行时,即拥有线程池;而第三种方式声明了new_current_thread()表示只利用单一线程.对于多线程的运行时,如果没有设置线程数,则默认为硬件CPU的核数.(Rust的线程与系统线程是1:1的)

fn main() {
    //let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
    //let rt=tokio::runtime::Runtime::new().unwrap();
    let rt=tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
    std::thread::sleep(std::time::Duration::from_secs(30));
}

使用默认值测试一下使用的线程数,cargo run之后用ps -eLf|grep XXX查看

image-20230728150230858

如果觉得写这么一长串runtime初始化太麻烦,也可以试试类似于注解的方式,这两种创建方式是等价的.

#[tokio::main(worker_threads=10)]
//#[tokio::main(flavor="current_thread")]
fn main() {
    std::thread::sleep(std::time::Duration::from_secs(30));
}

3. 异步任务

根据之前的知识,我们可以使用block_on()这种阻塞式的方法来执行异步任务.并且在之前那个异步服务器的例子中,我们就知道即使在异步中仍然可以利用spawn多线程并发完成多个异步任务.但是使用block_on()阻塞式仍然是不符合异步思想的,因此有另外一种不阻塞的进入运行时的方式enter().这个方法名字也很好理解进入某个运行时,实际上它只是一种上下文的声明代表着直到drop前包含的异步任务都在这个运行时上下文中执行.

use tokio::{self, runtime::Runtime, time};
use chrono::Local;
use std::thread;
​
fn now() -> String {
    Local::now().format("%F %T").to_string()
}
​
fn main() {
    let rt = Runtime::new().unwrap();
​
    // 进入runtime,但不阻塞当前线程
    let guard1 = rt.enter();
​
    // 生成的异步任务将放入当前的runtime上下文中执行
    tokio::spawn(async {
        time::sleep(time::Duration::from_secs(3)).await;
        println!("task1 sleep over: {}", now());
    });
​
    // 释放runtime上下文,这并不会删除runtime
    drop(guard1);
​
    // 可以再次进入runtime
    let guard2 = rt.enter();
    tokio::spawn(async {
        time::sleep(time::Duration::from_secs(1)).await;
        println!("task2 sleep over: {}", now());
    });
​
    drop(guard2);
​
    // 阻塞当前线程,等待异步任务的完成
    thread::sleep(std::time::Duration::from_secs(5));
}

image-20230728203541359

结果也证明了enter()并没有阻塞异步任务执行,减少了程序等待时间.

4. 计算还是IO

之前异步的那一篇就解释过异步和多线程的取舍问题,针对计算密集任务我们一般使用多线程,而IO密集则选择异步.幸运的是,Tokio也考虑到了这个问题.通常使用的运行时都是工作线程(worker thread),用于异步任务中.如果涉及到计算密集任务时,我们就需要使用Tokio中的阻塞线程(blocking thread)来占用CPU进行计算减小上下文切换开销.我们使用spawn_blocking()才能创建一个阻塞线程,来进行计算密集任务.不过阻塞线程虽然有阻塞二字,实际上并不会阻塞当前线程,它还是在运行时中的同时也可以进行异步操作.本质上,阻塞线程更像是为计算任务独开的线程,不会让其他任务进入阻塞线程,也不会转移阻塞线程内的任务,不会被调度打断任务执行,没有了上下文切换等开销达到线程性能最大化,开销最小化.

use std::thread;
use chrono::Local;
use tokio::{self, runtime::Runtime, time};
​
fn now() -> String {
    Local::now().format("%F %T").to_string()
}
​
fn main() {
    let rt1 = Runtime::new().unwrap();
    // 创建一个blocking thread,可立即执行(由操作系统调度系统决定何时执行)
    // 注意,不阻塞当前线程
    let task = rt1.spawn_blocking(|| {
        println!("in task: {}", now());
        // 注意,是线程的睡眠,不是tokio的睡眠,因此会阻塞整个线程
        thread::sleep(std::time::Duration::from_secs(3))
    });
​
    // 小睡1毫秒,让上面的blocking thread先运行起来
    std::thread::sleep(std::time::Duration::from_millis(1));
    println!("not blocking: {}", now());
    
    rt1.block_on(async{
        // 生成一个blocking thread和一个独立的thread
        // block on不会阻塞等待两个线程终止,因此block_on在这里会立即返回
        rt1.spawn_blocking(|| std::thread::sleep(std::time::Duration::from_secs(2)));
        thread::spawn(||std::thread::sleep(std::time::Duration::from_secs(2)));
    });
    
    // 可在runtime内等待blocking_thread的完成
    rt1.block_on(async {
        task.await.unwrap();
        println!("after blocking task: {}", now());
    });
}

image-20230728205825760

从结果来看,最终线程只休眠了3s,也就是task.await的时间.至于在block_on()中创建阻塞线程或者普通线程是不会阻塞等待线程执行的.

5. 关闭runtime

有三种关闭运行时的方式

  • drop() 立即关闭未被阻塞的工作线程,对于阻塞线程以及阻塞的工作线程仍然会执行,因此这样会阻塞当前线程.
  • shutdown_timeout() 等待一个timeout后强行终止运行时中的所有线程
  • shutdown_background() 立即强行关闭运行时