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