A stack-less Rust coroutine library under 100 LoC
Posted January 25, 2020 ‐ 4 min read
As of stable Rust 1.39.0, it is possible to
implement a very basic and safe
coroutine library using Rust's
async
/await
support, and
in under 100 lines of code. The implementation depends solely on std
and is
stack-less (meaning, not depending on a separate CPU architecture stack).
A very basic simple coroutine library contains only an event-less 'yield' primitive, which stops execution of the current coroutine so that other coroutines can run. This is the kind of library that I chose to demonstrate in this post to provide the most concise example.
Yielder
To the coroutine we pass a Fib
struct that only contains a simple binary
state. This Fib
struct has a waiter
method that creates a
Future
that the
coroutine can use in order to be await
ed upon.
use Future;
use Pin;
use ;
|
Executor
Our executor keeps a vector of uncompleted futures, where the state of each
future is located on the heap. As a very basic executor, it only supports
adding futures to it before actual execution takes place and not afterward.
The push
method adds a closure to the list of futures, and the run
method
performs interlaced execution of futures until all of them complete.
use VecDeque;
|
Null Waker
For the executor implementation above, we need a null Waker, similar to the one used in genawaiter (link).
use ,
const RAW_WAKER: RawWaker = new;
const VTABLE: RawWakerVTable = new;
unsafe
unsafe
unsafe
unsafe
|
Giving it a go
We can test the library using a program such as the following:
|
The output is as follows:
Running
1 A
2 A
3 A
1 B
2 B
3 B
1 C
2 C
3 C
1 D
2 D
3 D
Done
|
Performance
Timing the following program compiled with lto = true
, I have seen that it
takes about 5 nanoseconds for each iteration of the internal loop, on an Intel
i7-7820X CPU.
|
End notes
One of the nice things about the async
/await
support in the Rust compiler
is that it does not depend on any specific run-time. Thus, if you commit to
certain run-time, you are free to implement your own executor.
Independency of run-time has its downsides. For example, the library presented
here is not compatible with other run-times such as
async-std
. And in fact, the implementation
violates the interface intended for the Future
's poll
function by assuming
that the Future
will always be Ready
after it was Pending
.
Combined uses of several run-times in a single program is possible but requires extra care (see Reddit discussion).