tokio 基础知识学习

1. 创建 tokio Runtime

直接创建:

//默认的工作线程数量将和CPU核数(虚拟核,即CPU线程数)相同
let rt = tokio::runtime::Runtime::new().unwrap();

//单线程
tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();

//指定线程数
let rt = tokio::runtime::Builder::new_multi_thread()
    .worker_threads(11)
    // .enable_io() // 异步IO
    // .enable_time() // 计时器
    .build()
    .unwrap();

使用注解:

#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
async fn main() {
}

//等价于
fn main(){
  tokio::runtime::Builder::new_multi_thread()
        .worker_threads(10)  
        .enable_all()
        .build()
        .unwrap()
        .block_on(async { ... });
}

PS:mac 下查看线程数的方法

# 返回值减去1,就是总线程数,因为多统计了一个标题行。
ps -M $(pgrep axum) | wc -l 

可在不同线程内创建互相独立的runtime:

use std::thread;
use std::time::Duration;
use tokio::runtime::Runtime;

//对于4核8线程的电脑,此时总共有19个OS线程:16个worker-thread,2个spawn-thread,一个main-thread。
fn main() {
  // 在第一个线程内创建一个多线程的runtime
  let t1 = thread::spawn(||{
    let rt = Runtime::new().unwrap();
    thread::sleep(Duration::from_secs(10));
  });

  // 在第二个线程内创建一个多线程的runtime
  let t2 = thread::spawn(||{
    let rt = Runtime::new().unwrap();
    thread::sleep(Duration::from_secs(10));
  });

  t1.join().unwrap();
  t2.join().unwrap();
}

注:runtime实现了Send和Sync这两个Trait,因此可以将runtime包在Arc里,然后跨线程使用同一个runtime。

2. 执行异步任务

block_on 的一些特性:
① 参数是一个Future,可以用 async {} 来定义一个 Future,每一个 Future 都是一个已经定义好但尚未执行的异步任务
② 会阻塞当前线程(例如阻塞住上面的main函数所在的主线程),直到其指定的 异步任务树(可能有子任务) 全部完成
③ 有返回值,其返回值为其所执行异步任务的返回值

fn main() {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        println!("before sleep: {}", Local::now().format("%F %T.%3f"));
        //只等待异步任务,故只等了5秒,而不是15秒
        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
        time::sleep(time::Duration::from_secs(15));
        println!("after sleep: {}", Local::now().format("%F %T.%3f"));
    });
}
use tokio::{time, runtime::Runtime};

fn main() {
    let rt = Runtime::new().unwrap();
    //返回值是 3
    let res: i32 = rt.block_on(async{
      time::sleep(time::Duration::from_secs(2)).await;
      3
    });
    println!("{}", res);  // 3
}

3.向 Runtime 添加异步任务

fn now() -> String {
    Local::now().format("%F %T").to_string()
}

fn main() {
    let rt1 = Runtime::new().unwrap();
    rt1.block_on(async {
        println!("create an async task: {}", now());
        let task = tokio::spawn(async {
            time::sleep(time::Duration::from_secs(10)).await;
            println!("async task over: {}", now());
        });
        task.await.expect("async task is failed");
    });
}

4.使用 enter 进入 Runtime

block_on 会阻塞当前线程。而使用 enter 不会阻塞,并且返回一个 EnterGuard,作用是声明从它开始的所有异步任务都将在runtime上下文中执行,直到删除该EnterGuard。

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(5)).await;
      println!("task1 sleep over: {}", now());
    });

    // 释放runtime上下文,这并不会删除runtime
    drop(guard1);

    // 可以再次进入runtime
    let guard2 = rt.enter();
    tokio::spawn(async {
      time::sleep(time::Duration::from_secs(4)).await;
      println!("task2 sleep over: {}", now());
    });

    drop(guard2);

    // 阻塞当前线程,等待异步任务的完成
    thread::sleep(std::time::Duration::from_secs(10));
}

5.使用 blocking thread

单个线程或多个线程的runtime,指的都是工作线程,即只用于执行异步任务的线程,这些任务主要是IO密集型的任务。tokio 默认会将每一个工作线程均匀地绑定到每一个CPU核心上。如果要运行一个阻塞的任务,可以使用阻塞线程。

创建阻塞线程有两种方式,第一种是 thread::spawn(),即普通线程创建的阻塞线程;另外一种是 rt.spawn_blocking(),由 runtime 创建的阻塞线程(不被 tokio 所调度,但可以对它使用一些异步操作如await),和 rt.block_on() 区别是它不会阻塞当前线程。

类别 方法 特性 备注
进入运行时 rt.block_on() 阻塞当前主线程
派生工作线程 rt.spawn() rt 派生新工作线程 被 tokio 调度
派生普通线程 thread::spawn() 普通线程派生的新阻塞线程 不被 tokio 所调度
派生阻塞线程 rt.spawn_blocking() rt 派生新阻塞线程 不进入异步队列,不被 tokio 所调度

注:① 在 block_on() 中生成了blocking thread 或普通的线程,block_on() 不会等待这些线程的完成。② 在 block_on() 中使用 std::thread::sleep() 会阻塞当前工作线程,而使用 tokio::time::sleep() 会直接放弃CPU,将工作线程交还给调度器,使该线程能够再次被调度器分配到其它异步任务。

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(10))
    });

    // 小睡1毫秒,让上面的 blocking thread 先运行起来
    std::thread::sleep(std::time::Duration::from_millis(1));

    println!("not blocking: {}", now());

    // 可在 runtime 内等待 blocking_thread 的完成
    rt1.block_on(async {
        task.await.unwrap();
        println!("after blocking task: {}", now());
    });
}

6.使用 tokio::task 创建绿色线程

Rust中的原生线程(std::thread)是 OS线程,每一个原生线程,都对应一个操作系统的线程。操作系统线程在内核层,由操作系统负责调度,缺点是涉及相关的系统调用,它有更重的线程上下文切换开销。

green thread 则是用户空间的线程,由程序自身提供的调度器负责调度,由于不涉及系统调用,同一个OS线程内的多个绿色线程之间的上下文切换的开销非常小,因此非常的轻量级。可以认为,它们就是一种特殊的协程。

函数名 功能描述
spawn 向 runtime 中添加新异步任务
spawn_blocking 生成一个 blocking thread 并执行指定的任务
block_in_place 在某个 worker thread 中执行同步任务,但是会将同线程中的其它异步任务转移走,使得异步任务不会被同步任务饥饿
yield_now 立即放弃 CPU,将线程交还给调度器,自己则进入就绪队列等待下一轮的调度
unconstrained 将指定的异步任务声明未不受限的异步任务,它将不受 tokio 的协作式调度,它将一直霸占当前线程直到任务完成,不会受到 tokio 调度器的管理
spawn_local 生成一个在当前线程内运行,一定不会被偷到其它线程中运行的异步任务

1> 使用 task::yield_now 干预线程调度

让当前任务立即放弃CPU,将worker thread 交还给调度器,任务自身则进入调度器的就绪队列等待下次被轮询调度。

use tokio::task;

#[tokio::main]
async fn main() {
    task::spawn(async {
        // ...
        println!("spawned task done!")
    });

    // Yield, allowing the newly-spawned task to execute first.
    task::yield_now().await;
    println!("main task done!");
}

2> 使用 task.abort() 取消任务

use tokio::{self, runtime::Runtime, time};
use tokio::task::JoinError;

fn main() {
    let rt = Runtime::new().unwrap();

    rt.block_on(async {
        let task = tokio::task::spawn(async {
            time::sleep(time::Duration::from_secs(10)).await;
        });

        // 让上面的异步任务跑起来
        time::sleep(time::Duration::from_millis(1)).await;
        // 取消任务。如果异步任务已经完成,再对该任务执行abort()操作将没有任何效果。
        task.abort();
        // 取消任务之后,可以取得JoinError
        let abort_err: JoinError = task.await.unwrap_err();
        println!("{}", abort_err.is_cancelled());
    })
}

3> 本地线程

use chrono::Local;
use tokio::{self, runtime::Runtime, time};

fn now() -> String {
    Local::now().format("%F %T").to_string()
}

fn main() {
    let rt = Runtime::new().unwrap();
    let local_tasks = tokio::task::LocalSet::new();

    // 向本地任务队列中添加新的异步任务,但现在不会执行
    local_tasks.spawn_local(async {
        println!("local task1");
        time::sleep(time::Duration::from_secs(5)).await;
        println!("local task1 done");
    });

    local_tasks.spawn_local(async {
        println!("local task2");
        time::sleep(time::Duration::from_secs(5)).await;
        println!("local task2 done");
    });

    println!("before local tasks running: {}", now());
    rt.block_on(async {
        // 开始执行本地任务队列中的所有异步任务,并等待它们全部完成
        local_tasks.await;
    });
}

7.处理多个线程

1> join! 等待多个任务执行

use chrono::Local;
use tokio::{self, runtime::Runtime, time};

fn now() -> String {
    Local::now().format("%F %T").to_string()
}

async fn do_one() {
    println!("doing one: {}", now());
    time::sleep(time::Duration::from_secs(2)).await;
    println!("do one done: {}", now());
}

async fn do_two() {
    println!("doing two: {}", now());
    time::sleep(time::Duration::from_secs(1)).await;
    println!("do two done: {}", now());
}

fn main() {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        tokio::join!(do_one(), do_two());// 等待两个任务均完成,才继续向下执行代码
        println!("all done: {}", now());
    });
}

2> try_join! 等待多任务执行,并处理异常

async fn do_stuff_async() -> Result<(), &'static str> {
    // async work
}

async fn more_async_work() -> Result<(), &'static str> {
    // more here
}

#[tokio::main]
async fn main() {
    let res = tokio::try_join!(do_stuff_async(), more_async_work());

    match res {
         Ok((first, second)) => {
             // do something with the values
         }
         Err(err) => {
            println!("processing failed; error = {}", err);
         }
    }
}

3> select! 轮询

select!的作用是等待第一个完成的异步任务并执行对应任务完成后的操作。通常与 loop 循环结合使用。

注:select!本身是【阻塞】的,只有select!执行完,它后面的代码才会继续执行。

use tokio::{self, runtime::Runtime, time::{self, Duration}};

async fn sleep(n: u64) -> u64 {
    time::sleep(Duration::from_secs(n)).await;
    n
}

fn main() {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        tokio::select! {
          v = sleep(5) => println!("sleep 5 secs, branch 1 done: {}", v),
          v = sleep(3) => println!("sleep 3 secs, branch 2 done: {}", v),
        };

        println!("select! done");
    });
}

4> joinSet 的使用

use tokio::task::JoinSet;

#[tokio::main]
async fn main() {
    let mut set = JoinSet::new();

    // 创建10个异步任务并收集
    for i in 0..10 {
        // 使用JoinSet的spawn()方法创建异步任务
        set.spawn(async move { i });
    }

    // join_next()阻塞直到其中一个任务完成
    set.join_next().await;
    set.abort_all();

    //等待所有线程执行完毕
    // while let Some(_) = set.join_next().await {
    // 
    // }

    //使用JoinSet的abort_all()或直接Drop JoinSet,都会对所有异步任务进行abort()操作。
    // set.abort_all();

    //使用JoinSet的shutdown()方法,将先abort_all(),然后join_next()所有任务,直到任务集合为空。
    // set.shutdown().await;
}

8.tokio Timer 的使用

#[tokio::main]
async fn main() {
    //休眠
    // std::thread::sleep() 会阻塞当前线程,而tokio的睡眠不会阻塞当前线程,
    // 仅仅只是立即放弃CPU,等待睡眠时间终点到了之后被唤醒。
    // tokio的sleep的睡眠精度是毫秒
    time::sleep(time::Duration::from_secs(3)).await;
    time::sleep_until(time::Instant::now() + time::Duration::from_secs(2)).await;

    //超时
    let res = time::timeout(time::Duration::from_secs(5), async {
        println!("sleeping: {}", Local::now().format("%F %T"));
        time::sleep(time::Duration::from_secs(6)).await;
        33
    });
    match res.await {
        Err(_) => println!("task timeout: {}", Local::now().format("%F %T")),
        Ok(data) => println!("get the res '{}': {}", data, Local::now().format("%F %T")),
    };

    //间隔
    // 使用 tokio::time::interval() 或 tokio::time::interval_at() 定义定时器
    // 定义计时器时,要将其句柄(即计时器变量)声明为mut,因为每次tick时,都需要修改计时器内部的下一次计时起点。
    // 不像其它语言中的间隔计时器,tokio的间隔计时器需要手动调用tick()方法来生成临时的异步任务。
    // 删除计时器句柄可取消间隔计时器。
    let mut intv = time::interval_at(time::Instant::now(), Duration::from_secs(1));
    //设置策略,计时延迟的策略
    intv.set_missed_tick_behavior(MissedTickBehavior::Skip);
    time::sleep(Duration::from_secs(10)).await;
    intv.tick().await;
    intv.tick().await;
}

参考资料


https://rust-book.junmajinlong.com/ch100/01_understand_tokio_runtime.html#创建tokio-runtime

posted on 2023-10-24 23:24  Lemo_wd  阅读(382)  评论(0编辑  收藏  举报

导航