从 tokio-core 到 tokio
主线位置:这一页不在主线上。它把任务主线和 IO 主线放回 2016 到 2018 年的 tokio-core 里走一遍,再给读老代码、老博客的人一张对照表。
tokio-core 是什么
tokio-core 是 tokio 这个 crate 出现之前的运行时核心。仓库的第一个提交是 2016-07-30,最后一个版本 0.1.18 只有一个提交:2020-12-03 的 #330,把 README 第一句改成:
This crate is deprecated in favor of [Tokio](http://github.com/tokio-rs/tokio).
仓库随后归档。0.1.18 没有打 tag,下面链接 0.1.18 的源码时用这个提交的哈希 fdba3f1;讲早期独立实现时用 tag 0.1.12,那是它自己实现事件循环的最后一个版本。
它建在两样东西上:futures 0.1(Future trait、Task 句柄、Stream/Sink)和 mio 0.6(对 epoll/kqueue 的封装)。0.1.12 的模块:
| 模块 | 内容 |
|---|---|
reactor | Core(事件循环)、Handle、Remote、PollEvented(把任意 mio::Evented 接进循环)、Timeout、Interval |
net | TcpListener、TcpStream、UdpSocket,以及 UDP 的 UdpCodec/UdpFramed |
io | Io trait、Codec/Framed/EasyBuf,copy、read_exact 等组合子。0.1.5 起整个模块标成 deprecated,理由是 moved to the tokio-io crate(src/io/mod.rs 第 12 行) |
channel | 0.1.1 起 deprecated,让用 futures::sync::mpsc |
一个冷知识:tokio 仓库本身就是从 tokio-core 仓库长出来的。tokio 仓库在 2017-11-06 和 2018-01-26 两次把 tokio-core 的 master 合并进自己的 new-crate 分支,所以在今天的 tokio 仓库里 git log,能一路追到 tokio-core 2016-07-30 那个第一个提交。
一个 Core 里有什么
pub struct Core {
events: mio::Events,
tx: mpsc::UnboundedSender<Message>,
rx: RefCell<Spawn<mpsc::UnboundedReceiver<Message>>>,
_rx_registration: mio::Registration,
rx_readiness: Arc<MySetReadiness>,
inner: Rc<RefCell<Inner>>,
// ...
_future_registration: mio::Registration,
future_readiness: Arc<MySetReadiness>,
}
struct Inner {
id: usize,
io: mio::Poll,
// Dispatch slabs for I/O and futures events
io_dispatch: Slab<ScheduledIo>,
task_dispatch: Slab<ScheduledTask>,
// ...
timer_heap: Heap<(Instant, usize)>,
timeouts: Slab<(Option<Slot>, TimeoutState)>,
}
一个 Core 就是一个线程上的全部家当:一个 mio::Poll、一张 IO 资源表、一张任务表、一个定时器最小堆。共享状态放在 Rc<RefCell<Inner>> 里,所以 Core 不能跨线程。
两个句柄的区别全在字段上(第 97 行起):
| 句柄 | 字段 | 能不能跨线程 | 能做什么 |
|---|---|---|---|
Handle | Remote 加一个 Weak<RefCell<Inner>> | 不能(Weak 是 !Send) | 创建 IO 对象、直接往任务表里塞任务 |
Remote | 循环 ID 加一个 mpsc::UnboundedSender<Message> | 能 | 只能发消息,由循环线程自己处理 |
一切都是 mio 事件。 mio::Token 是这样分配的(第 142 行):
| token | 含义 |
|---|---|
| 0 | Remote 发来的消息队列 |
| 1 | 传给 Core::run 的那个 future |
| 2 + 2k | 第 k 个 IO 资源 |
| 2 + 2k + 1 | 第 k 个 spawn 出来的任务 |
任务也有 token,因为每个 spawn 出来的任务在 Inner::spawn 里都会拿到一个自己的 mio::Registration。它不对应任何文件描述符,是 mio 0.6 提供的「用户态就绪源」:别人调它配对的 SetReadiness::set_readiness,下一次 mio::Poll::poll 就会把它当成一个事件返回。唤醒一个任务,就是在它的 registration 上置一次「可读」(MySetReadiness 第 796 行):
impl Notify for MySetReadiness {
fn notify(&self, _id: usize) {
self.0.set_readiness(mio::Ready::readable())
.expect("failed to set readiness");
}
}
Core::run 怎么驱动 futures 0.1
先回忆 futures 0.1 和今天的区别。0.1 的 Future 有两个关联类型 Item 和 Error,poll(&mut self) 返回 Result<Async<Item>, Error>,Async 是 Ready(T) 或 NotReady。poll 没有 Context 参数:执行器在 poll 之前把「当前任务」放进线程本地变量,future 想挂起时调 futures::task::current() 拿到一个 Task 句柄存起来,事件来了调 task.notify()。执行器这一侧,executor::spawn(f) 把 future 包成 Spawn<F>,poll_future_notify(¬ify, id) 负责设置那个线程本地变量并 poll 一次,notify 是一个实现了 Notify trait 的对象,task.notify() 最终调到它。
Core::run 就是这个模型最直接的写法:
pub fn run<F>(&mut self, f: F) -> Result<F::Item, F::Error>
where F: Future,
{
let mut task = executor::spawn(f);
let mut future_fired = true;
loop {
if future_fired {
let res = try!(CURRENT_LOOP.set(self, || {
task.poll_future_notify(&self.future_readiness, 0)
}));
if let Async::Ready(e) = res {
return Ok(e)
}
}
future_fired = self.poll(None);
}
}
注意 F: Future 后面既没有 Send 也没有 'static:future 就在 run 的栈帧上,不会离开这个线程。传给它的 Notify 是 token 1 对应的 future_readiness,所以「run 的 future 被唤醒」也是一个 mio 事件。
self.poll(None) 是循环的一轮(第 257 行):用定时器堆顶算出最长能睡多久,调 mio::Poll::poll 睡下去;醒来后先触发到期的定时器,再逐个分发事件:token 0 就去消费消息队列,token 1 就返回 true 让 run 再 poll 一次,其余的偶数 token 进 dispatch_io,奇数 token 进 dispatch_task,后者把任务从表里取出来 poll_future_notify 一次。
一次读操作的全过程
TcpStream 内部是 PollEvented<mio::net::TcpStream>。它实现 std::io::Read 的方式是:没就绪就返回 WouldBlock,读到 WouldBlock 就登记兴趣(poll_evented.rs 第 274 行):
impl<E: Read> Read for PollEvented<E> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if let Async::NotReady = self.poll_read() {
return Err(io::ErrorKind::WouldBlock.into())
}
let r = self.get_mut().read(buf);
if is_wouldblock(&r) {
self.need_read();
}
return r
}
}
need_read 清掉就绪位,再调 IoToken::schedule_read:
pub fn schedule_read(&self, handle: &Remote) {
handle.send(Message::Schedule(self.token, task::current(), Direction::Read));
}
task::current() 就是在这里被调用的:当前任务的句柄被装进一条消息,交给循环。上层的 future(比如 tokio_io::io::read_exact)看到 WouldBlock,把它翻译成 Async::NotReady 往上返回。把整条线连起来:
sequenceDiagram participant F as future participant P as PollEvented participant C as Core 循环 participant M as mio Poll C->>F: poll_future_notify F->>P: read P->>P: 没数据,WouldBlock,need_read P->>C: Message Schedule,带上 task current C->>C: Inner schedule,把 Task 存进 reader 槽 F-->>C: 返回 Async NotReady C->>M: poll,睡下去 M-->>C: socket 可读,偶数 token C->>C: dispatch_io 取出 reader,task notify C->>M: notify 在任务的 Registration 上置可读 M-->>C: 下一轮 poll 返回任务的奇数 token C->>F: dispatch_task,再次 poll_future_notify F->>P: read 读到数据,返回 Ready
和今天的 tokio 对照,骨架其实没变:IO 资源上有一个 ScheduledIo 记下等待者(连名字都保留下来了,见 ScheduledIo 与就绪状态),事件来了叫醒它,再 poll 一次。变的是两处:
- 记下的是
Waker,从poll的Context参数里来,不再是线程本地的task::current()(见 poll 与唤醒)。 - 唤醒一个任务是把它放进运行队列,不再是在 mio 上制造一个假事件。只有队列空了,线程才 park 在驱动上(见 驱动栈与 park)。tokio-core 每唤醒一个任务都要绕
mio::Poll::poll一圈。
0.1.13:变成 tokio 的薄壳
tokio-core 的终点不在 0.1.18。真正的转折是 2018-03-08 的 #303,版本号随这个提交改成 0.1.13,提交说明是:
Tokio reform was released as a separate crate (`tokio`). However, the
ecosystem has been split as tokio-core is not compatible with tokio.
Libraries (like hyper) that expose tokio-core as part of the public API
cannot easily switch to tokio as is.
This patch updates tokio-core to use tokio internally.
此前一个月发生了这几件事:tokio 0.1.0 在 2018-02-07 发布,CHANGELOG 写的是「based on RFC」(tokio-rfcs 第 3 号);2018-02-12 tokio-core 的 README 加上「scheduled for deprecation」,只修 bug,新功能全进 tokio。问题是 hyper 这样的库在公开 API 里暴露了 tokio-core 的类型,没法说换就换。于是 tokio-core 反过来,把自己的 API 原样保留,内部全部换成 tokio 的实现。
依赖的变化最直观。0.1.12 只依赖 futures、mio、slab、tokio-io 等;0.1.13 加了 tokio、tokio-executor、tokio-reactor;0.1.17(2018-04-11,#320)再加 tokio-timer,同时删掉自己的定时器堆(heap.rs 一个文件 305 行)。0.1.18 的依赖(Cargo.toml 第 22 行起):
mio = "0.6.12"
scoped-tls = "0.1.0"
iovec = "0.1"
tokio-io = "0.1"
tokio = "0.1.5"
tokio-executor = "0.1.2"
tokio-reactor = "0.1.1"
tokio-timer = "0.2.1"
futures = "0.1.21"
再看 0.1.18 的 Core,mio::Poll 和两张表都不见了:
pub struct Core {
/// Uniquely identifies the reactor
id: usize,
/// Handle to the Tokio runtime
rt: tokio::runtime::Runtime,
/// Executes tasks
executor: RefCell<CurrentThread<Timer<ParkThread>>>,
/// Timer handle
timer_handle: timer::Handle,
// ...
// Shared inner state
inner: Rc<RefCell<Inner>>,
}
Core::new 在第 129 行调 tokio::runtime::Runtime::new(),建了一整个 tokio 0.1 运行时。tokio 0.1.5 的 Runtime 是「后台线程上的反应器 + 任务窃取线程池」,Builder::build 里那行注释写得很直白(tokio 0.1.5 runtime/builder.rs 第 93 行):
// Spawn a reactor on a background thread.
let reactor = Reactor::new()?.background()?;
所以 0.1.13 之后,Core 名字还叫「事件循环」,实际上已经不是反应器了:
flowchart TB
subgraph old["0.1.12:Core 自己就是反应器"]
direction TB
o1["Core::run 所在线程"] --> o2["mio::Poll"]
o2 --> o3["io_dispatch:IO 资源"]
o2 --> o4["task_dispatch:每个任务一个 Registration"]
o1 --> o5["timer_heap:自己的定时器堆"]
end
subgraph shell["0.1.13 起:Core 是 tokio 0.1 的外壳"]
direction TB
n1["Core::run 所在线程"] --> n2["CurrentThread 执行器:跑 Handle::spawn 的任务"]
n2 --> n3["Timer + ParkThread:只睡觉和计时,不碰 IO"]
n4["tokio 0.1 Runtime"] --> n5["后台反应器线程:所有 socket 注册在这里"]
n4 --> n6["线程池:跑 spawn_send 和 tokio::spawn 的任务"]
n5 -. "notify 唤醒" .-> n2
end
几处细节:
- IO 注册在别的线程上。
TcpListener::bind(&addr, &handle)最终是PollEvented2::new_with_handle(listener, handle.new_tokio_handle())(net/tcp.rs第 134 行),而new_tokio_handle返回的就是那个后台反应器的句柄。socket 就绪时,后台线程通过 futures 0.1 的notify唤醒任务,CurrentThread再把停在ParkThread上的Core线程叫醒。 Handle::spawn还是单线程。 第 502 行:先放进pending_spawn,下一轮循环交给CurrentThread的spawn_local(循环正在跑任务、RefCell借不到时直接spawn_local)。所以它仍然只要'static,不要Send。- 新增的出口。 #303 同时加了
Handle::spawn_send(把Send的 future 丢进线程池)、Core::runtime、Handle::new_tokio_handle,以及TcpListener::bind2这类不要&Handle、用默认反应器的构造函数。这些都是给渐进迁移留的桥。 Core::run里能直接用 tokio。run在 poll 之前套了三层with_default:反应器、执行器(线程池)、定时器。这是 0.1.15 的 #314、#315 加的,修的就是「在Core::run的 future 里调tokio::spawn不能用」。
此后的版本都是小修:0.1.14(2018-03-09)修 PollEvented,0.1.15(2018-03-11)修执行上下文和远程队列,0.1.16(2018-03-23)修「清错了就绪位」,0.1.17 换定时器。0.1.18 只改了三个文件(增 5 行、删 6 行):Cargo.toml 的版本号、README 和 lib.rs 文档里的弃用声明。
为什么拆成小 crate,又为什么合回去
tokio 0.1 把 tokio-core 拆成了一组 crate。tokio 0.1.5 的依赖是 tokio-io、tokio-executor、tokio-reactor、tokio-threadpool、tokio-tcp、tokio-udp、tokio-timer 七个(tokio 0.1.5 Cargo.toml)。当时的提交说明交代了理由:
- 反应器单独成 crate(#169,2018-03-02):自己实现 IO 资源的库需要
Registration、PollEvented,单独一个 crate 就不用依赖整个 tokio。 - 执行器和反应器分家(#141,2018-02-21):运行时 = 任务窃取线程池 + 后台反应器。
tokio-executor在提交说明里被称为「a hopefully short lived crate」,它基于当时 futures 0.2 的执行器 RFC。 - 默认反应器、懒绑定(#160,2018-02-28):提交说明回顾了 tokio-core 的做法,「Originally, tokio-core required that I/O resources be explicitly associated with a reactor upon creation by passing in a
&Handle」,reform 引入默认反应器,IO 资源第一次被 poll 时才绑定。今天TcpListener::bind(addr)不用传句柄,就是从这里来的。
0.2.0(2019-11-26)把它们收了回来,CHANGELOG 写的是「most tokio-* crates are collapsed into this crate」「A single I/O / timer is used per runtime」「tokio::codec is moved to tokio-util」。合并是逐个 crate 做的,提交说明都是同一句「A step towards collapsing Tokio sub crates into a single tokio crate (#1318)」。合并的总体理由写在 #1318 这个 issue 里,本页没有核实它的原文,下面只引提交说明里能读到的部分:
| 子 crate | 合并进 tokio 的 PR | 日期 |
|---|---|---|
| tokio-fs | #1672 | 2019-10-21 |
| tokio-timer | #1674 | 2019-10-21 |
| tokio-net(由 tokio-reactor 改名而来,#1450) | #1683 | 2019-10-25 |
| tokio-io | #1691 | 2019-10-26 |
| tokio-executor | #1702 | 2019-10-28 |
| tokio-sync | #1705 | 2019-10-29 |
这些提交说明里还有一句:「Functionality can be opted out of by using the various ... feature flags」。也就是说,「只用一部分」这个需求从 crate 边界挪到了 feature 边界,拆 crate 当初想解决的问题,用 feature 也能解决,还不用协调版本。codec 是唯一的例外,#1675 把它挪进新建的 tokio-util,理由是这类「less stable」的 API 需要「a greater rate of breaking changes」,不该拖着 tokio 本体一起改大版本。今天各 feature 拉进哪些依赖,见 crate 依赖图。
整条时间线:
flowchart TB a["2016-07-30 tokio-core 第一个提交"] --> b["2017-02-05 io 模块挪到 tokio-io,tokio-core 0.1.5 起标 deprecated"] b --> c["2018-02-07 tokio 0.1.0,按 reform RFC 发布"] c --> d["2018-02-12 tokio-core README 预告弃用,只修 bug"] d --> e["2018-03-08 tokio-core 0.1.13,内部改用 tokio"] e --> f["2018-04-11 tokio-core 0.1.17,定时器换成 tokio-timer"] f --> g["2019-11-26 tokio 0.2.0,子 crate 合并回 tokio,codec 进 tokio-util"] g --> h["2020-12-03 tokio-core 0.1.18,只加弃用声明"] h --> i["2020-12-23 tokio 1.0.0"]
对照 tokio 1.54.1
Core → Runtime / LocalRuntime
Core::run(f) 对应 Runtime::block_on,两者对 f 都只要求 Future,不要 Send、不要 'static。单线程的 Core 对应 Builder::new_current_thread(见 Builder、Runtime 与 Handle、current_thread)。
但 current_thread 运行时不等于 Core:它的 Handle 是 Send 的,可以从别的线程往里 spawn,所以 tokio::spawn 在 current_thread 上也要求 Send。真正一对一的是 LocalRuntime:单线程、自身 !Send、有 block_on 和 spawn_local。
Handle::spawn → spawn_local
Handle::spawn 的约束是 F: Future<Item=(), Error=()> + 'static,不要 Send。今天的对应物是 task::spawn_local,约束是 F: Future + 'static,同样不要 Send,但只能在 LocalSet 或 LocalRuntime 里调用(见 JoinSet、LocalSet、task_local)。在普通运行时上用 tokio::spawn 去 spawn 一个持有 Rc 的 future,会得到 future cannot be sent between threads safely。
下面用 LocalRuntime 写一个 tokio-core 风格的程序:多个任务共享一个 Rc<Cell<_>>。
use std::cell::Cell;
use std::rc::Rc;
fn main() {
// Core::new() 的对应物:单线程、!Send 的 LocalRuntime
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build_local(Default::default())
.unwrap();
// handle.spawn(...) 的对应物:future 可以持有 Rc
let hits = Rc::new(Cell::new(0));
let handles: Vec<_> = (0..3)
.map(|_| {
let hits = hits.clone();
rt.spawn_local(async move { hits.set(hits.get() + 1) })
})
.collect();
// core.run(...) 的对应物
rt.block_on(async {
for h in handles {
h.await.unwrap();
}
});
println!("hits = {}", hits.get());
}
输出 hits = 3(M2 Max、rustc 1.99.0、tokio 1.54.1,跑 3 次结果相同)。
还有一处不同:tokio-core 的 spawn 返回 (),任务的结果和 panic 都拿不回来,签名上也就要求 Item=(), Error=()。tokio 0.2 起 spawn 返回 JoinHandle(见 JoinHandle、取消与 JoinError)。
Remote → Handle
Remote 是能跨线程的那个句柄,今天对应 Runtime::handle() 拿到的 Handle。区别在 spawn 的约束上。Remote::spawn 是:
pub fn spawn<F, R>(&self, f: F)
where F: FnOnce(&Handle) -> R + Send + 'static,
R: IntoFuture<Item=(), Error=()>,
R::Future: 'static,
跨线程的是闭包,闭包在循环线程上执行、造出 future,所以 future 本身可以 !Send。Handle::spawn 要求 future 本身 Send + 'static,没有这个两段式。今天形状完全相同的 API 在 tokio-util 里:LocalPoolHandle::spawn_pinned,约束是 F: FnOnce() -> Fut + Send + 'static、Fut: Future + 'static。
Remote::handle() 的作用是「如果当前就在循环线程上,把 Remote 升级成 Handle」,靠的是线程本地变量 CURRENT_LOOP。今天对应 Handle::current() / Handle::try_current(),同样读线程本地上下文(见 线程本地上下文)。
IO 对象不再要 &Handle
tokio-core 里每个 IO 对象创建时都要传 &Handle:TcpListener::bind(&addr, &handle)、Timeout::new(dur, &handle)。今天是从线程本地上下文里找当前运行时,找不到就 panic,报错原文是 there is no reactor running。这是 2018 年 #160 引入默认反应器时付出的代价:不用到处传句柄,但「在哪个运行时上」变成了隐式的。
反应器本身也从「每个 Core 一个、只服务这个线程」变成「每个运行时一个、所有 worker 共用」(见 反应器)。自己实现 IO 资源用的公开类型 PollEvented,今天在 tokio 内部是 pub(crate),对外的是 AsyncFd。
futures 0.1 → std::future + Waker
最大的变化在 trait 本身:poll 多了 self: Pin<&mut Self> 和 cx: &mut Context<'_> 两个参数,「当前任务」从线程本地变量变成了显式参数。上面那条读操作的链,在今天是 Registration::poll_ready 把 cx.waker() 存进 ScheduledIo,过程见 从 TcpStream::read 到挂起。
codec → tokio-util
0.1.12 的 tokio_core::io::Codec(frame.rs 第 258 行)有 In、Out 两个类型,decode(&mut EasyBuf)、encode(Out, &mut Vec<u8>)。它先挪到 tokio-io 的 codec 模块,2019 年又挪进 tokio-util。今天拆成 Decoder 和 Encoder<Item> 两个 trait,缓冲区用 bytes::BytesMut,Framed 实现 futures 0.3 的 Stream + Sink。
定时器
Timeout::new(dur, &handle) 返回的是一个「到点完成」的 future,相当于今天的 tokio::time::sleep,不是今天的 tokio::time::timeout(后者包住另一个 future,限它的时间)。名字一样、意思不同,读老代码最容易在这里看错。Interval::new(dur, &handle) 对应 tokio::time::interval。实现上,0.1.12 是一个最小堆,今天是分层时间轮(见 分层时间轮)。
读老代码的对照表
0.1.18 的 crate 文档里有一个 echo 服务器(lib.rs 第 57 行)。下面是 tokio-core 0.1.18 时期的写法,不能在今天的 tokio 上编译:
fn main() {
// Create the event loop that will drive this server
let mut core = Core::new().unwrap();
let handle = core.handle();
// Bind the server's socket
let addr = "127.0.0.1:12345".parse().unwrap();
let listener = TcpListener::bind(&addr, &handle).unwrap();
// Pull out a stream of sockets for incoming connections
let server = listener.incoming().for_each(|(sock, _)| {
// Split up the reading and writing parts of the
// socket
let (reader, writer) = sock.split();
// A future that echos the data and returns how
// many bytes were copied...
let bytes_copied = copy(reader, writer);
// ... after which we'll print what happened
let handle_conn = bytes_copied.map(|amt| {
println!("wrote {:?} bytes", amt)
}).map_err(|err| {
println!("IO error {:?}", err)
});
// Spawn the future as a concurrent task
handle.spawn(handle_conn);
Ok(())
});
// Spin up the server on the event loop
core.run(server).unwrap();
}
逐行翻译成 tokio 1.54.1(cargo check 通过):
use tokio::io::copy;
use tokio::net::TcpListener;
fn main() -> std::io::Result<()> {
// Core::new()
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
// core.run(server)
rt.block_on(async {
// TcpListener::bind(&addr, &handle):不再传 handle
let listener = TcpListener::bind("127.0.0.1:12345").await?;
// listener.incoming().for_each(...)
loop {
let (mut sock, _) = listener.accept().await?;
// handle.spawn(handle_conn)
tokio::spawn(async move {
let (mut reader, mut writer) = sock.split();
// copy(reader, writer).map(...).map_err(...)
match copy(&mut reader, &mut writer).await {
Ok(amt) => println!("wrote {amt:?} bytes"),
Err(err) => println!("IO error {err:?}"),
}
});
}
})
}
组合子链(map、map_err、and_then)变成了 .await 加 match 或 ?,这是 async/await 带来的,和运行时无关。和运行时有关的只有四行注释标出的地方。
完整对照:
| tokio-core / futures 0.1 | tokio 1.54.1 / std |
|---|---|
Core::new() | Builder::new_current_thread().enable_all().build(),要 !Send 任务就 .build_local(...) |
core.run(f) | rt.block_on(f) |
core.turn(max_wait) | 没有公开对应物,运行时不让外部单步 |
core.handle()(!Send) | LocalRuntime 本身,或在 LocalSet 里直接调 spawn_local |
core.remote()(Send) | rt.handle().clone() |
handle.spawn(f) | tokio::task::spawn_local(f) |
handle.spawn_fn(|| ...) | spawn_local(async move { ... }) |
handle.spawn_send(f)(0.1.13 起) | tokio::spawn(f) |
remote.spawn(|h| f) | handle.spawn(f)(f 须 Send),或 LocalPoolHandle::spawn_pinned(|| f) |
remote.handle() | Handle::try_current() |
TcpListener::bind(&addr, &handle) | TcpListener::bind(addr).await |
listener.incoming() | loop { listener.accept().await } |
TcpStream::connect(&addr, &handle) | TcpStream::connect(addr).await |
reactor::Timeout::new(dur, &handle) | tokio::time::sleep(dur) |
reactor::Interval::new(dur, &handle) | tokio::time::interval(dur) |
reactor::PollEvented<E> | tokio::io::unix::AsyncFd<T> |
tokio_core::io::{Codec, Framed, EasyBuf}、tokio_io::codec | tokio_util::codec::{Decoder, Encoder, Framed}、bytes::BytesMut |
tokio_io::{AsyncRead, AsyncWrite}(read 返回 WouldBlock) | tokio::io::{AsyncRead, AsyncWrite}(poll_read(cx, buf)) |
futures::sync::mpsc、tokio_core::channel | tokio::sync::mpsc |
Future<Item = T, Error = E> | Future<Output = Result<T, E>> |
fn poll(&mut self) -> Poll<T, E> | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> |
Ok(Async::Ready(v)) / Ok(Async::NotReady) | Poll::Ready(v) / Poll::Pending |
futures::task::current() | cx.waker().clone() |
task.notify() | waker.wake() / waker.wake_by_ref() |
executor::Notify + Spawn::poll_future_notify | std::task::Wake + Waker::from(Arc<_>) |
spawn 返回 (),要求 Item=(), Error=() | spawn 返回 JoinHandle<T> |
小结
- tokio-core 的
Core是一个线程上的完整事件循环:一个mio::Poll、IO 表、任务表、定时器堆,状态放在Rc<RefCell<_>>里。Handle不能跨线程,Remote能,但只能发消息。 - 它驱动 futures 0.1 的方式是「一切都是 mio 事件」:连唤醒任务都是在任务自己的
mio::Registration上置一次可读。当前任务通过线程本地的task::current()取得,不是参数。 - 转折点是 2018-03-08 的 0.1.13(#303):API 不变,内部换成 tokio 0.1 的运行时,IO 注册到后台反应器线程,
Core只剩一个CurrentThread执行器。0.1.18 只是加了弃用声明。 - tokio 0.1 为了「只依赖一部分」拆成七个子 crate,0.2.0 又用 feature 代替 crate 边界合了回来,只有 codec 进了 tokio-util。
- 对照今天:
Core→LocalRuntime或 current_threadRuntime,Handle::spawn→spawn_local,Remote→Handle,task::current()→cx.waker(),Timeout→sleep。