Crate puteketeke

source ·
Expand description

§Async Executor

Build Status codecov Lines of Code

This crate is what you might build if you were to add a thread pool to smol, and tasks to start paused.

I take somewhat the opposite approach to Tokio. (I assume, I’ve never actually used Tokio.) I believe that Tokio assumes that you want all of your code to be async, whereas I do not.

I take the approach that most of the main code is synchronous, with async code as needed. This is exactly the situation I found myself in when I wanted to add async support to my language, dwarf.

§Hello World

// Create an executor with four threads.
let executor = Executor::new(4);

let task = executor
    .create_task(async { println!("Hello world") })
    .unwrap();

// Note that we need to start the task, otherwise it will never run.
executor.start_task(&task);
future::block_on(task);

§Motivation

Primarily this crate is intended to be used by those that wish to have some async in their code, and don’t want to commit fully. This crate does not require wrapping main in ’async fn`, and it’s really pretty simple. The features of note are:

  • an RAII executor, backed by a threadpool, that cleans up after itself when dropped
  • a task that starts paused, and is freely passed around as a value
  • the ability to create isolated workers upon which tasks are executed
  • an ergonomic and simple API
  • parallel timers

Practically speaking, the greatest need that I had in dwarf, besides an executor, was the ability to enqueue a paused task, and pass it around. It’s not really too difficult to explain why either. Consider this dwarf code:

async fn main() -> Future<()> {
    let future = async {
        print(42);
    };
    print("The answer to life the universe and everything is: ");
    future.await;
}

As you might guess, this prints “The answer to life the universe and everything is: 42”.

The interpreter is processing statements and expressions. When the block expression:

async {
    print(42);
}

is processed, we don’t want the future to start executing yet, otherwise the sentence would be wrong.

What we need is to hold on to that future and only start it when it is awaited, after the print expression. Furthermore, we need to be able to pass that future around the interpreter as we process the subsequent statements and expressions.

§Notes

§Timers

For reasons that we have yet to ascertain specifically, timers only work properly on one executor. What is apparently happening is that Timer::after(n) is remembering the maximal n across workers. This causes all timers to fire at the max of the current value or the previous maximum.

For this reason we’ve included an Executor::timer method that will create a timer on the root worker. Unfortunately this requires capturing a clone of the executor to pass into your future.

Additionally concurrent timer accuracy is a function of the number of threads. Based on empirical testing, there is a non-linear relationship between the timer / thread ratio and the timer error. When timers == threads the error is 0. As the number of concurrent timers increases the error in the duration increases. I plan on working out exactly what is happening, and what I can do about it.

§The Future

I fully intend to add priorities to tasks. I have in fact begun work, but it’s stalled on some sticky ownership issues. I may in fact end up taking another approach to the one that I’ve begun. In any case, felt that it was better to publish what I have now, and then iterate on it.

Been there.

Done that.

Got the t-shirt.

It was sort of neat to make happen, but the tasks are dispatched too quickly to make a difference.

I plan to see what I can do about sending &Workers around, rather than owned copies.

Structs§

Enums§