Rust in Everything
基于 tokio 1.54.1

从 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 的模块:

模块内容
reactorCore(事件循环)、Handle、Remote、PollEvented(把任意 mio::Evented 接进循环)、Timeout、Interval
netTcpListener、TcpStream、UdpSocket,以及 UDP 的 UdpCodec/UdpFramed
ioIo trait、Codec/Framed/EasyBuf,copy、read_exact 等组合子。0.1.5 起整个模块标成 deprecated,理由是 moved to the tokio-io crate(src/io/mod.rs 第 12 行)
channel0.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 里有什么

先看 0.1.12 的 Core 和它的 Inner:

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 行起):

句柄字段能不能跨线程能做什么
HandleRemote 加一个 Weak<RefCell<Inner>>不能(Weak 是 !Send)创建 IO 对象、直接往任务表里塞任务
Remote循环 ID 加一个 mpsc::UnboundedSender<Message>能只能发消息,由循环线程自己处理

一切都是 mio 事件。 mio::Token 是这样分配的(第 142 行):

token含义
0Remote 发来的消息队列
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(&notify, 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 一次。变的是两处:

  1. 记下的是 Waker,从 poll 的 Context 参数里来,不再是线程本地的 task::current()(见 poll 与唤醒)。
  2. 唤醒一个任务是把它放进运行队列,不再是在 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#16722019-10-21
tokio-timer#16742019-10-21
tokio-net(由 tokio-reactor 改名而来,#1450)#16832019-10-25
tokio-io#16912019-10-26
tokio-executor#17022019-10-28
tokio-sync#17052019-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.1tokio 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::codectokio_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::channeltokio::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_notifystd::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_thread Runtime,Handle::spawn → spawn_local,Remote → Handle,task::current() → cx.waker(),Timeout → sleep。

💬 关联讨论

发起讨论
加载中...