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 awaited 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).