在学习tokio,其指南文章地址Async in depth | Tokio - An asynchronous Rust runtimehttps://tokio.rs/tokio/tutorial/async中介绍tokio运行时原理,如果轮询future的,完整代码地址:https://github.com/tokio-rs/website/blob/master/tutorial-code/mini-tokio/src/main.rshttps://github.com/tokio-rs/website/blob/master/tutorial-code/mini-tokio/src/main.rs下面是我翻译的代码:
cargo依赖下面两个库。
futures = "0.3.17"
crossbeam="0.8.1"
//! 演示了如何实现一个(非常)基本的异步rust执行器和定时器。
//! 本文件的目的是提供一些关于各种构件如何结合的背景。
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::{Duration, Instant};
// 一个允许我们实现`std::task::Waker`的工具,而不使用`unsafe`代码。
use futures::task::{self, ArcWake};
// 用作排队调度任务的通道。
use crossbeam::channel;
// 主入口。一个mini-tokio实例被创建,一些任务被产生出来。
// 我们的mini-tokio实现只支持生成任务和设置延迟。
fn main() {
// 创建mini-tokio实例.
let mini_tokio = MiniTokio::new();
// 产生根任务. 所有其他任务都是从这个根任务的上下文中产生的。
// 在调用`mini_tokio.run()`之前,没有任何工作发生。
mini_tokio.spawn(async {
//产生一个任务
spawn(async {
// 等待100ms时间,以便执行"hello "之后打印"world"。
delay(Duration::from_millis(100)).await;
println!("world");
});
// 产生第二个任务,打印hello
spawn(async {
println!("hello");
});
// 我们还没有实现执行器关闭,所以要强制进程退出。
// 在根任务中延迟200ms
delay(Duration::from_millis(200)).await;
println!("await");
//强制退出
std::process::exit(0);
});
// 启动mini-tokio执行器循环。等调度的任务被接收并执行。
mini_tokio.run();
}
/// 一个非常基本的基于通道的future执行器。
/// 当任务被唤醒时,它们被安排在通道的发送部分排队。
/// 执行者在接收端等待并执行收到的任务。
///
/// 当一个任务被执行时,通道的发送部分会通过任务的Waker传递。
struct MiniTokio {
// 接收预定的任务。
// 当一个任务被安排好后,相关的future就可以取得进展了。
// 这通常发生在任务使用的资源准备好进行操作的时候。
// 例如,一个套接字收到了数据,一个`读'的调用将成功。
scheduled: channel::Receiver<Arc<Task>>,
// 调度通道的发送者.
sender: channel::Sender<Arc<Task>>,
}
impl MiniTokio {
/// 初始化一个mini-tokio实例.
fn new() -> MiniTokio {
let (sender, scheduled) = channel::unbounded();
MiniTokio { scheduled, sender }
}
/// 在mini-tokio实例上产生一个根future。
///
/// 把给定的根future将被包裹成"任务",然后放入"调度"通道队列。
/// 当`run'被调用时,根future将被执行。
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
Task::spawn(future, &self.sender);
}
/// 运行执行器。
///
/// 这将启动执行器循环并无限期地运行。
/// 没有实现关闭机制。
///
/// 任务从"调度"通道接收器中弹出。
/// 在通道上接收到一个任务标志着该任务已经准备好被执行。
/// 这发生在任务第一次被创建和它的唤醒者被使用时。
fn run(&self) {
// 设置CURRENT thread-local,使其指向当前的执行器。
// Tokio使用线程本地变量来实现`tokio::spwan`。
// 当进入运行时,执行器用线程-本地存储必要的上下文,以支持产生新任务。
CURRENT.with(|cell| {
*cell.borrow_mut() = Some(self.sender.clone());
});
// 执行器循环。调度的任务被接收。
// 如果通道是空的,线程就会阻塞,直到有任务被接收。
while let Ok(task) = self.scheduled.recv() {
// 执行任务,直到它完成或无法取得进一步进展,并返回`Poll::Pending`。
task.poll();
}
}
}
//相当于`tokio::spawn`。
// 当进入mini-tokio执行器时,`CURRENT`线程本地被设置为指向该执行器的通道的Send half。
// 然后,spwn需要为给定的`future`创建`Task`线束,并将其推入计划队列。
pub fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
CURRENT.with(|cell| {
//通过线程变量方案传递了sender
let borrow = cell.borrow();
let sender = borrow.as_ref().unwrap(); //因为例子上都是在一个线程中,而且run运行在前,会把线程变更初始化完,unwrap不会报错
Task::spawn(future, sender);
});
}
// 与`thread::sleep`异步等效。在这个函数上的等待会在给定的时间内暂停。
//
// mini-tokio通过生成一个`定时器线程`来实现延迟,该线程在所设定的时间内休眠,并在延迟完成后通知调用者。
// 每次调用"delay"就会产生一个线程,这显然是一个糟糕的实现策略,没有人应该在`生产中`使用这个策略。
// Tokio并没有使用这种策略。
// 然而,它很简单地用几行代码来实现,所以我们在这里用于演示。
async fn delay(dur: Duration) {
// `delay`是一个`叶子`的future。有时,这被称为 "资源"。
// 其他资源包括`套接字`和`通道`。
// `资源`可能无法用`async/await`来实现,因为它们必须与一些操作系统的细节相结合。
// 正因为如此,我们必须手动实现叶子"future",当你在把一个同步资源异步化时经常会这样干。
//
// 然后,将API暴露为`async fn'是很好的。一个好用的习惯是手动定义一个私有的future,然后用一个公共的`async fn`API中使用它。
struct Delay {
// delay时长.
when: Instant,
// 延迟完成后用于通知的唤醒者。
// 唤醒者必须能被定时器线程和`future`线程访问,所以它被`Arc<Mutex<_>'包裹起来。
waker: Option<Arc<Mutex<Waker>>>,
}
impl Future for Delay {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
// 首先,如果这是第一次调用future,则产生定时器线程。
// 如果定时器线程已经在运行,确保保存的`Waker'与当前任务的Waker相匹配。
if let Some(waker) = &self.waker {
let mut waker = waker.lock().unwrap();
// 检查保存的waker是否与当前任务的waker一致。
// 这是必要的,`Delay'的future实例可能会移到到不同的任务被`poll`。
// 如果发生这种情况,给定的`Context'所包含的waker就会不同,必须更新我们保存的waker同步这种变化。
if !waker.will_wake(cx.waker()) {
*waker = cx.waker().clone(); //更新
}
} else {
let when = self.when;
let waker = Arc::new(Mutex::new(cx.waker().clone()));
self.waker = Some(waker.clone());
// 这是第一次调用`poll`,产生定时器线程。
thread::spawn(move || {
println!("thread id is {:?}", thread::current().id()); //验证是每次会另启一个线程
let now = Instant::now();
if now < when {
thread::sleep(when - now); //休眠
}
// 持续时间已经过了。通过调用唤醒器通知调用者(最终会通知到执行器,然后再次poll)。
let waker = waker.lock().unwrap();
waker.wake_by_ref(); //通知唤醒,会唤醒到Task实例,Task实例会调用自己的sender字段通知执行器
});
}
// 一旦唤醒者被存储起来,定时器线程被启动,就是检查延迟是否已经完成的时候了。
// 这是通过检查当前的瞬间完成的。
// 如果持续时间已经过了,那么future就已经完成了,`Poll::Ready`将被返回。
if Instant::now() >= self.when {
Poll::Ready(())
} else {
// 持续时间没有过去,future没有完成,所以返回`Poll::Pending`。
//
// `Future`Trait契约要求,当返回`Pending`时,future确保一旦future应该再次轮询,就会向给定的唤醒者发出信号。
// 在我们的例子中,通过在这里返回`Pending',我们承诺一旦请求的持续时间结束,我们将调用包括在`Context'参数中的指定唤醒者。
// 我们是通过上面的`定时器线程`内部调用waker方法来确保这一点。
//TODO 如果我们忘记调用`唤醒器`,任务将无限期地挂起。这就是自己库在转化异步时都做到的承诺
Poll::Pending
}
}
}
// 创建我们`Delay`的future.
let future = Delay {
when: Instant::now() + dur,
waker: None,
};
//等待一段`时间`完成.
future.await;
}
// 用于跟踪当前的mini-tokio实例,以便`spawn'函数能够安排产生的任务。
thread_local! {
static CURRENT: RefCell<Option<channel::Sender<Arc<Task>>>> =
RefCell::new(None);
}
// 任务。包裹future以及future被唤醒后安排的必要数据。
struct Task {
// 用一个"Mutex"包裹着future,使整个`Task`结构体是`Sync`。
// 将只有一个线程能使用`future`。
//TODO Tokio的运行时通过使用"unsafe"代码来避免使用Mutex(性能问题)。也不用使用`Box`包裹了。
future: Mutex<Pin<Box<dyn Future<Output = ()> + Send>>>,
// 当一个任务被通知时,它被发送这个通道。
// 执行器的发送者会弹出被通知的任务并执行它们。
executor: channel::Sender<Arc<Task>>,
}
impl Task {
// 通过一个future产生一个task .
// 初始化一个新的包含给定future的任务,并用`sender`发送到`执行器`的调度通道中。
// 然后通道的接收方将获得该任务并执行它。
fn spawn<F>(future: F, sender: &channel::Sender<Arc<Task>>)
where
F: Future<Output = ()> + Send + 'static,
{
let task = Arc::new(Task {
future: Mutex::new(Box::pin(future)),
executor: sender.clone(),
});
let _ = sender.send(task);
}
// 执行一个调度任务。这将创建必要的`task::Context`,包含任务的waker。
// 这个waker将任务推送到mini-tokio调度通道中。然后用waker轮询future。
fn poll(self: Arc<Self>) {
// 获取任务的waker引用,因为Task实现了ArcWaker
let waker = task::waker(self.clone());
//用waker初始化task的context.
let mut cx = Context::from_waker(&waker);
// 这将永远不会阻塞,因为只有一个线程锁定了future.
let mut future = self.future.try_lock().unwrap();
// 轮询future
let _ = future.as_mut().poll(&mut cx);
}
}
// 标准库提供了低级别的、不安全的API来定义waker。
// 我们不用写unsafe的代码,而是使用由`futures`库定义一个能够让我们的`Task`结构体使用的waker。
impl ArcWake for Task {
fn wake_by_ref(arc_self: &Arc<Task>) {
// 把任务的发送到`执行器`任务调度通道中执行。`执行器`会从通道接收并轮询任务。
println!("waker task,then send task to executor");
let _ = arc_self.executor.send(arc_self.clone());
}
}
浏览完上面的代码,从main函数入口开始:
mini_tokio先产生了第一个future,也叫根future, mini_tokio的spawn方法就是对Task的spawn方法封装。Task::spawn方法就是把future和需要的参数(本例子中主要是执行器通道的发送者)包装成一个Task,然后通过执行器的通道的发送者发送到通道中等执行。
这个根future里面的内容先两次调用了普通函数spawn,这个spawn函数也对Task::spawn的封装,只是这里是通过线程本地变量来共享了执行器任务队列通道的发送者。然后是调用了delay异步函数后调用.await表达式,然后执行std::process::exit(0);
下面main函数中继续执行mini_tokio.run方法,run方法里面先是对线程本地变量进行了赋值,然后开始从通道中获取Task。
这时我们知道上面mini_tokio.spawn创建了一个根future,然后包装成了Task,并且送到了执行器的调度任务通道中,所以执行器循环接收任务通道中的第一个任务就是根future包装task。然后调用task的poll方法,而task的poll方法就是包装调用future的poll方法。对于根future的第一次poll调用, 就开始执行到了两个spawn方法(这时获取线程变量不会报错的,所以直接unwrap了),也就是会创建两个task并且发送到执行器调度任务队列通道中(因为本例中执行器的实现是单线程的,其实就是main线程,虽然两个任务已经发到通道中,但是并不会被执行器立即接收轮询)。然后继续向下执行到delay异步方法,这个异步方法是对一个"叶子"future的包装,.await轮询结果时,Delay结构的Future在持续时间未结束,所以会返回Pending,未准备好,也就是根future虽然有进展,但并未完成,所以本次根任务轮询结束。
这时执行器又从队列中获取一个任务,在本例中肯定是在轮询根任务future时创建的第一个future,这个future代码会创建一个休眠100ms的子future并且等这个`休眠`future执行结果,然后打印world.
执行器轮询第一个future时,这个future又会调用子future(delay包装future),而这个子future并未完成,也返回了Pending,第一个future轮询结束。
执行器继续接收到了第二个future任务,这个future就是简单打印一句hello代码,执行完毕,所以返回Ready,本future也不会被其它引用,也不会再发送到任务通道,ARC引用计数为零,该future销毁。
执行器又开始从通道中获取任务来轮询,这时(未超过100ms)通道中并没有任务,所以执行器阻塞,过了100ms后, 第一个future的子future关联定时器线程从休眠中结束,通过waker调用wake_by_ref方法,通知它的调用者(它的调用者是Task而不是执行器),Task实现了ArcWaker,ArcWaker的wake_by_ref方法里就是把自己再发送到执行器的任务队列中,所以在Delay的Future实现中wake_by_ref方法后就是把自己重新加到执行器任务队列中。 这时执行器不再阻塞,获取到通道中的第一个future任务,再次轮询,执行println("world");然后第一个future完成。
执行器继续阻塞等待下一个任务,又过了100ms,根future的delay的"叶子"future的定时器线程从休眠中结束,然后唤醒调用者,把自己加入执行器的队列中, 再次被执行器轮询,这一次会调用println!("await");和std::process::exit(0);然后退出程序。
原文中是直接调用标准库代码退出了,我加了一句println("await");,执行完这一行代码,然后程序直接退出了(如果不调用std::process::exit(0);执行器会继续一直接等从通道中获取任务,但我们这个例子所有的任务都执行完了,所以程序永远不会退出)。
“叶子”Future,Tokio对标准库异步原语的实现大部分都是叶子Future。一般是库实现者需要来实现"叶子"future。知道“叶子“future很重要,虽然你可能不实现"叶子"future,但你对异步轮询细节就不会有死角(好多文章都不介绍),future内部实现大致类似上面Delay的Future实现。