运行时模型

使用Tokio编写的应用程序组织在大量小的非阻塞任务中。 Tokio任务类似于goroutine或者Erlang进程,但是是非阻塞的。它们设计为轻量级,可以快速生成,并保持较低的调度开销。它们也是非阻塞的,因为无法立即完成的此类操作必须立即返回。它们返回一个表示操作正在进行的值,而不是返回操作的结果,表明操作正在进行中。

非阻塞执行

使用Future trait实现Tokio任务:

  1. struct MyTask {
  2. my_resource: MyResource,
  3. }
  4. impl Future for MyTask {
  5. type Item = ();
  6. type Error = ();
  7. fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
  8. match self.my_resource.poll() {
  9. Ok(Async::Ready(value)) => {
  10. self.process(value);
  11. Ok(Async::Ready(()))
  12. }
  13. Ok(Async::NotReady) => Ok(Async::NotReady),
  14. Err(err) => {
  15. self.process_err(err);
  16. Ok(Async::Ready(()))
  17. }
  18. }
  19. }
  20. }

使用tokio :: spawn或通过调用executor对象上的Spawn方法将任务提交给 executorpoll函数驱动任务。没有调用poll就什么都不做。在任务上调用poll直到Ready(())返回是 executor的工作。

MyTask将从my_resource接收一个值并处理它。一旦值处理完毕,任务就完成了他的逻辑并结束。这会返回Ok(Async :: Ready(()))

为了完成处理,任务取决于my_resource提供的值。鉴于my_resource是一个非阻塞任务,它在调用my_resource.poll()时,可能准备好或者还没准备好提供值。如果它准备就绪,它返回Ok(Async :: Ready(value))。如果没有准备好,它会返回Ok(Async::NotReady)

当资源未准备好提供值时,这意味着该任务本身还没准备好完成,任务的poll函数也返回NotReady

在未来的某个时刻,资源将随时准备提供值。资源使用任务系统向 executor发信号给executor通知它已准备好。 executor安排任务,导致MyTask :: poll又叫了一遍。这一次,假设my_resource准备就绪,那么值就是从my_resource.poll()返回并且任务完成。

协作调度

协作调度用于在 executor上调度任务。单个 executor将通过一小组线程管理许多任务。将有比线程更多的任务。这也没有抢占。这个意味着当任务被安排执行时,它会阻止当前线程直到poll函数返回。

因此,实现poll在很短的时间内执行才是重要的。对于I / O绑定的应用程序,通常会发生这种情况。但是,如果任务预计必须长时间运行,则应该推迟工作到blocking pool或将计算分解为更小的块和在每个块执行之后yield回来。

任务系统

任务系统是资源通知executor准备就绪的系统。 任务由消耗资源的非阻塞逻辑组成。 在上面的示例中,MyTask使用单个资源my_resource,但没有限制任务可以使用的资源数量。

当任务正在执行并尝试使用未准备好的资源时,它在该资源上被逻辑阻塞,即任务无法进一步处理,直到资源准备就绪。 Tokio跟踪阻塞当前任务的资源以进行推进。当一个依赖资源准备就绪, executor安排任务。这是通过跟踪当任务在资源中表现兴趣完成。

MyTask执行,尝试使用my_resourcemy_resource返回NotReady时,MyTask隐含表示对my_resource资源感兴趣。对此,任务和资源是连接的。什么时候资源准备就绪,任务再次被安排。

task :: current和Task :: notify

通过两个API完成跟踪兴趣并通知准备情况的变化:

  • task::current
  • Task::notify
    当调用my_resource.poll()时,如果资源准备就绪,则立即返回值而不使用任务系统。如果资源没有准备好,通过调用task::current() -> Task 来获取当前任务的句柄。这是通过读取executor设置的线程局部变量集获得此句柄。

一些外部事件(在网络上接收的数据,后台线程完成计算等…)将导致my_resource准备好生成它的值。那时,准备好my_resource的逻辑将调用从task :: current获得的任务句柄上的notify。这个表示准备就绪会改变 executorexecutor随后安排任务执行。

如果多个任务表示对资源感兴趣,则只有last任务这样做会得到通知。资源旨在从单一任务使用。

Async :: NotReady

任何返回Async的函数都必须遵守contract(契约)。 当返回NotReady,当前任务必须已经注册准备就绪的变更通知。 以上部分讨论了资源的含义。 对于任务逻辑,这意味着无法返回NotReady除非资源已返回“NotReady”。 通过这样做,contract得到了传承。 当前任务已注册通知,因为已从资源收到NotReady

必须非常小心避免在没有从资源收到NotReady的情况下返回NotReady。 例如,以下任务中,任务实现结果永远不会完成。

  1. use futures::{Future, Poll, Async};
  2. enum BadTask {
  3. First(Resource1),
  4. Second(Resource2),
  5. }
  6. impl Future for BadTask {
  7. type Item = ();
  8. type Error = ();
  9. fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
  10. use BadTask::*;
  11. let value = match *self {
  12. First(ref mut resource) => {
  13. try_ready!(resource.poll())
  14. }
  15. Second(ref mut resource) => {
  16. try_ready!(resource.poll());
  17. return Ok(Async::Ready(()));
  18. }
  19. };
  20. *self = Second(Resource2::new(value));
  21. Ok(Async::NotReady)
  22. }
  23. }

上面实现的问题是Ok(Async :: NotReady)是在将状态转换为Second后立即返回。 在这转换中,没有资源返回NotReady。 当任务本身返回时NotReady,它违反了contract ,因为任务将来不会被通知。

通常通过添加循环来解决这种情况:

  1. use futures::{Future, Poll, Async};
  2. fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
  3. use BadTask::*;
  4. loop {
  5. let value = match *self {
  6. First(ref mut resource) => {
  7. try_ready!(resource.poll())
  8. }
  9. Second(ref mut resource) => {
  10. try_ready!(resource.poll());
  11. return Ok(Async::Ready(()));
  12. }
  13. };
  14. *self = Second(Resource2::new(value));
  15. }
  16. }

思考它的一种方法是任务的poll函数不能返回,直到由于其资源不能进一步取得进展而准备就绪或明确yields(见下文)。

另请注意,返回Async函数只能从一个任务调用。 换句话说,这些函数只能从已经提交给tokio :: spawn或其他任务spawn函数调用

Yielding

有时,任务必须返回NotReady而不是在资源上被阻塞。这通常发生在运行计算很大且任务想要的时候将控制权交还 executor以允许其执行其他 future

Yielding 是通过通知当前任务并返回“NotReady”完成:

  1. use futures::task;
  2. use futures::Async;
  3. // Yield the current task. The executor will poll this task next
  4. // iteration through its run list.
  5. task::current().notify();
  6. return Ok(Async::NotReady);

Yield可用于分解CPU昂贵的计算:

  1. struct Count {
  2. remaining: usize,
  3. }
  4. impl Future for Count {
  5. type Item = ();
  6. type Error = ();
  7. fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
  8. while self.remaining > 0 {
  9. self.remaining -= 1;
  10. // Yield every 10 iterations
  11. if self.remaining % 10 == 0 {
  12. task::current().notify();
  13. return Ok(Async::NotReady);
  14. }
  15. }
  16. Ok(Async::Ready(()))
  17. }
  18. }

executor

executor员负责驱动完成许多任务。任务是产生于 executor之上, 是在executor需要调用它的poll函数的时候。 executor挂钩到任务系统以接收资源准备通知。

通过将任务系统与 executor实现分离,具体执行和调度逻辑可以留给 executor实现。tokio提供两个executor实现,每个实现具有独特的特点:current_threadthread_pool

当任务首次在executor之上生成时, executorSpawn将其包装。这将任务逻辑与任务状态绑定(这主要是遗留原因所需要的)。 executor通常会将任务存储在堆,通常是将它存储在BoxArc中。当 executor选择一个执行任务,它调用Spawn :: poll_future_notify。此函数确保将任务上下文设置为线程局部变量像task :: current能够读取它。

当调用poll_future_notify时, executor也是传递通知句柄和标识符。这些参数包含在由task :: current返回的任务句柄中,也是有关任务与executor连接的方式。

notify句柄是Notify 的实现,标识符是 executor用于查找当前任务的值。当调用Task::notifynotify函数使用提供的标识符调用notify句柄。该函数的实现负责执行调度逻辑。

实现 executor的一种策略是将每个任务存储在Box和使用链接列表来跟踪计划执行的任务。当调用Notify :: notify,然后将与之关联的任务标识符被推送到scheduled链表的末尾。当 executor运行时,它从链表的前端弹出并执行任务如上所述。

请注意,本节未介绍 executor的运行方式。细节这留给 executor实现。一个选项是 executor产生一个或多个线程并将这些线程专用于排出scheduled链表。另一个是提供一个MyExecutor :: run函数阻塞当前线程并排出scheduled链表。

资源,drivers和运行时

资源是叶子futures,即未以其他futures实施的futures。它们是使用上述任务系统的类型与 executor互动。资源类型包括TCP和UDP套接字,定时器,通道,文件句柄等.Tokio应用程序很少需要实现资源。相反,他们使用Tokio或第三方包装箱提供的资源。

通常,资源本身不能起作用而是需要drivers。例如,Tokio TCP套接字由Reactor支持。Reactor是socket资源driver。单个driver可以为大量资源实例提供动力。要使用该资源,drivers必须在某处运行这个过程。 Tokio提供网络资源的drivers(tokio-reactor),文件资源(tokio-fs)和定时器(tokio-timer)。提供解耦driver组件允许用户选择他们想要使用的组件。每个driver可以单独使用或与其他driver结合使用。

正因为如此,为了使用Tokio并成功执行任务,一个应用程序必须启动 executor和必要的drivers作为应用程序的任务依赖的资源。这需要大量的样板。为了管理样板,Tokio提供了几个运行时选项。运行时是与所有必需drivers捆绑在一起的executor,以便为Tokio的资源提供动力。不是单独管理所有各种Tokio组件,而是在一次调用中创建并启动运行时。

Tokio提供并发运行时单线程运行时。并发运行时基于多线程、工作窃取 executor。单线程运行时执行当前线程上的所有任务和drivers。用户可以选择最适合应用的运行时。

Future

如上所述,任务是使用Future trait实现的。 这个特点不仅限于实施任务。 一个 Future是表示一个非阻塞计算的值在未来的某个时间完成。 任务是一个计算没有输出。 Tokio中的许多资源都用Future实现。 例如,超时是Future在达到截止日期后完成。

trait包括许多与Future值一起工作的有用的组合器。

通过对应用特定类型实现Future来构建应用或使用组合器来定义应用程序逻辑。 通常两者兼而有之策略是最成功的。