Runtime Model
Now we will go over the Tokio / futures runtime model. Tokio is built on top ofthe crate and uses its runtime model. This allows it to interopwith other libraries also using the crate.
Note: This runtime model is very different than async libraries found inother languages. While, at a high level, APIs can look similar, the way codegets executed differs.
Synchronous Model
First, let’s talk briefly about the synchronous (or blocking) model. This is themodel that the Rust uses.
When socket.read
is called, either the socket has pending data in its receivebuffer or it does not. If there is pending data, then the call to read
willreturn immediately and buf
will be filled with that data. However, if there isno pending data, then the read
function will block the current thread untildata is received. At which time, buf
will be filled with this newly receiveddata and the read
function will return.
In order to perform reads on many different sockets concurrently, a thread persocket is required. Using a thread per socket does not scale up very well tolarge numbers of sockets. This is known as the c10k problem.
Non-blocking sockets
The way to avoid blocking a thread when performing an operation like read is tonot block the thread! When the socket has no pending data in its receive buffer,the read
function returns immediately, indicating that the socket was “notready” to perform the read operation.
Another way to think about a non-blocking read is as “polling” the socket fordata to read.
Polling Model
The strategy of polling a socket for data can be generalized to any operation.For example, a function to get a “widget” in the polling model would looksomething like this:
This function returns an where is an enum ofReady(Widget)
or NotReady
. The Async
enum is provided by the crate and is one of the building blocks of the polling model.
Now, lets define an asynchronous task without combinators that uses thispoll_widget
function. The task will do the following:
- Acquire a widget.
- Terminate the task.
To define a task, we implement theFuture
trait.
The key thing to note is, when MyTask::poll
is called, it immediately tries toget the widget. If the call to poll_widget
returns NotReady
, then the taskis unable to make further progress. The task then returns NotReady
itself,indicating that it is not ready to complete processing.
The task implementation does not block. Instead, “sometime in the future”, theexecutor will call again. poll_widget
will be called again. Ifpoll_widget
is ready to return a widget, then the task, in turn, is ready toprint the widget. The task can then complete by returning Ready
.
Executors
Executors are responsible for repeatedly calling poll
on a task until Ready
is returned. There are many different ways to do this. For example, theCurrentThread
executor will block the current thread and loop through allspawned tasks, calling poll on them. schedules tasks across a threadpool. This is also the default executor used by the runtime.
All tasks must be spawned on an executor or no work will be performed.
At the very simplest, an executor could look something like this:
Of course, this would not be very efficient. The executor spins in a busy loopand tries to poll all tasks even if the task will just return NotReady
again.
Ideally, there would be some way for the executor to know when the “readiness”state of a task is changed, i.e. when a call to poll
will return Ready
.Then, the executor would look something like this:
Being able to get notified when a task goes from “not ready” to “ready” is thecore of the task model.