Module task

Module task 

Source
Expand description

Asynchronous multitasking.

This module provides utilities for working with asynchronous tasks.

A task is a light weight, non-blocking unit of execution. Tasks allow you to cooperatively perform work in the background without blocking other code from running.

  • Tasks are light weight. Because tasks are scheduled and managed by vexide, creating new tasks or switching between tasks does not require a context switch and has fairly low overhead. Creating, running, and destroying large numbers of tasks is relatively cheap in comparison to traditional threads.

  • Tasks are scheduled cooperatively. Most operating system threads implement preemptive multitasking. This is a scheduling technique where the operating system allows each thread to run for a period of time, and then forcibly preempts it, temporarily pausing that thread and switching to another. Tasks, on the other hand, implement cooperative multitasking. In cooperative multitasking, a task will run until it voluntarily yields using an await point, giving control back to the vexide runtime’s scheduler. When a task yields by awaiting something, the vexide runtime switches to executing a different task.

  • Tasks are non-blocking. Typically, when an OS thread performs I/O or must synchronize with another thread, it blocks, allowing the OS to schedule another thread. When a task cannot continue executing, it should yield instead, allowing the vexide runtime to schedule another task in its place. Tasks should generally not perform operations that could block the CPU for a long period of time without an await point, as this would prevent other tasks from executing as well. This includes situations involving long-running “tight loops” (loop {}) without await points.

§Spawning Tasks

Perhaps the most important function in this module is spawn. This function can be thought of as an async equivalent to the standard library’s thread::spawn. It takes an async block or other future, and creates a new task that runs it concurrently in the background:

use vexide::task;

task::spawn(async {
    // perform some work here...
});

After a task is spawned, you are given a Task struct, representing a running (or previously running) task. The Task struct is itself a future which may be used to await the output of the spawned task. For example:

use vexide::task;

let task = task::spawn(async {
    // ...
    "hello world!"
});

// ...

// Await the result of the spawned task.
let result = task.await;
assert_eq!(result, "hello world!");

§Cancellation

When a Task is dropped, it will stop being polled by vexide’s runtime. This means that a task is cancelled when it leaves the scope it was spawned in.

use std::time::Duration;

use vexide::{task, time::sleep};

{
    // This task will never run, since it immediately falls out of scope after it's spawned.
    let task = task::spawn(async {
        loop {
            println!("Hiiiii :3");
            sleep(Duration::from_secs(1)).await;
        }
    });
}

If a task must outlive the scope it was spawned in, you can detach it. This lets the task run in the background beyond its current scope. When we detach a task, we lose its Task handle and therefore have no way to await its output. As a result, detached tasks may run forever with no way of being stopped.

use std::time::Duration;

use vexide::{task, time::sleep};

{
    let task = task::spawn(async {
        loop {
            println!("Hiiiii :3");
            sleep(Duration::from_secs(1)).await;
        }
    });

    // Run it forever, even after it leaves scope.
    task.detach();
}

§Sharing State Between Tasks

When running multiple tasks at once, it’s often useful to share some data between them.

To do this, we need multiple owners of the same piece of data, which is something that Rust’s borrow checker forbids. An easy way around this is to combine an Rc with a RefCell, which gives us both interior mutability and multiple owners. By wrapping our shared state in Rc<RefCell<T>>, we can clone a smart pointer to it across as many tasks as we want.

use std::{cell::RefCell, rc::Rc, time::Duration};

use vexide::{task, time::sleep};

let counter = Rc::new(RefCell::new(1));

// task_1 increments `counter` every second.
let task_1 = task::spawn({
    let counter = counter.clone();

    async move {
        loop {
            *counter.borrow_mut() += 1;
            sleep(Duration::from_secs(1)).await;
        }
    }
});

// task_2 prints `counter` every two seconds.
let task_2 = task::spawn(async move {
    loop {
        println!("Counter: {}", *counter.borrow());

        sleep(Duration::from_secs(2)).await;
    }
});

More complex use-cases may require you to hold ownership of shared state across await-points. In these cases, a simple Rc<RefCell<T>> will not suffice, since another running task may claim ownership of the data, which would cause the program to panic. Doing this effectively requires the use of a synchronization primitive like a Mutex or RwLock to manage safe access to shared state across multiple running tasks.

For more information on how to do this, see vexide’s sync module.

Macros§

task_local
Declares task-local variables in LocalKeys of the same names.

Structs§

LocalKey
A variable stored in task-local storage.

Functions§

spawn
Spawns a new async task that can be controlled with the returned task handle.

Type Aliases§

FallibleTask
A spawned task with a fallible response.
Task
A spawned task.