Expand description
Concurrent work-stealing deques.
These data structures are most commonly used in work-stealing schedulers. The typical setup involves a number of threads, each having its own FIFO or LIFO queue (worker). There is also one global FIFO queue (injector) and a list of references to worker queues that are able to steal tasks (stealers).
We spawn a new task onto the scheduler by pushing it into the injector queue. Each worker thread waits in a loop until it finds the next task to run and then runs it. To find a task, it first looks into its local worker queue, and then into the injector and stealers.
§Queues
Injector
is a FIFO queue, where tasks are pushed and stolen from opposite ends. It is
shared among threads and is usually the entry point for new tasks.
Worker
has two constructors:
new_fifo()
- Creates a FIFO queue, in which tasks are pushed and popped from opposite ends.new_lifo()
- Creates a LIFO queue, in which tasks are pushed and popped from the same end.
Each Worker
is owned by a single thread and supports only push and pop operations.
Method stealer()
creates a Stealer
that may be shared among threads and can only steal
tasks from its Worker
. Tasks are stolen from the end opposite to where they get pushed.
§Stealing
Steal operations come in three flavors:
steal()
- Steals one task.steal_batch()
- Steals a batch of tasks and moves them into another worker.steal_batch_and_pop()
- Steals a batch of tasks, moves them into another queue, and pops one task from that worker.
In contrast to push and pop operations, stealing can spuriously fail with Steal::Retry
, in
which case the steal operation needs to be retried.
§Examples
Suppose a thread in a work-stealing scheduler is idle and looking for the next task to run. To find an available task, it might do the following:
- Try popping one task from the local worker queue.
- Try stealing a batch of tasks from the global injector queue.
- Try stealing one task from another thread using the stealer list.
An implementation of this work-stealing strategy:
use crossbeam_deque::{Injector, Stealer, Worker};
use std::iter;
fn find_task<T>(
local: &Worker<T>,
global: &Injector<T>,
stealers: &[Stealer<T>],
) -> Option<T> {
// Pop a task from the local queue, if not empty.
local.pop().or_else(|| {
// Otherwise, we need to look for a task elsewhere.
iter::repeat_with(|| {
// Try stealing a batch of tasks from the global queue.
global.steal_batch_and_pop(local)
// Or try stealing a task from one of the other threads.
.or_else(|| stealers.iter().map(|s| s.steal()).collect())
})
// Loop while no task was stolen and any steal operation needs to be retried.
.find(|s| !s.is_retry())
// Extract the stolen task, if there is one.
.and_then(|s| s.success())
})
}
Structs§
- An injector queue.
- A stealer handle of a worker queue.
- A worker queue.
Enums§
- Possible outcomes of a steal operation.