Browse the docs

silk.concurrency.work_queue.

Queues that move owned values between native threads, for work-stealing schedulers.

When to use#

TypeUse it for
WorkDequeThe work of one thread, which idle threads can steal
InjectorA queue that all threads push to and take from
MailboxMany threads that send to one receiver

Details#

WorkDeque.make returns the Worker that owns a Chase-Lev deque. The worker pushes and pops at the bottom with no contention. Its Stealers take from the top with one compare-exchange. The ring grows by doubling. Old rings stay allocated until the deque is dropped, because a stealer can still read one.

Injector is a FIFO list under a Mutex. Mailbox is the Vyukov multi-producer, single-consumer queue: a send is one swap and one store, with no lock.

Each pushed value moves into its own allocation (a parcel), and the queues move parcel addresses atomically. Only the thread that wins a value reads it. Thus no thread copies a value that it does not own.

Errors#

Each push and send allocates, so it requires Allocator and can fail with OutOfMemoryError.

Gotchas#

Examples#

Steal work on another thread#

silk
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.core.effect { Effect }
import silk.concurrency.thread_native { NativeThread, SpawnError }
import silk.core.option { Option }
import silk.concurrency.work_queue { Steal, Stealer, WorkDeque }

// Takes the oldest value. A lost race gives Retry, so try again.
fn stealOne(stealer: Stealer<i32>) -> i32 {
  while true {
    match move stealer.steal() {
      Steal<i32>.Success { value } => {
        return value
      }
      Steal<i32>.Empty => {
        return 0
      }
      Steal<i32>.Retry => {}
    }
  }
  return 0
}

effect fn program() -> i32
  ! OutOfMemoryError | SpawnError {
  let mut allocator = Allocator.systemAllocatorProvider()
  let mut worker = run WorkDeque.make<i32>() |> Effect.provideMut<Allocator>(&mut allocator)
  run worker.push(1) |> Effect.provideMut<Allocator>(&mut allocator)
  run worker.push(2) |> Effect.provideMut<Allocator>(&mut allocator)
  let stealer = worker.stealer()
  let thief = run NativeThread.spawn(
    fn() -> i32 {
      return stealOne(move stealer)
    }
  )
    |> Effect.provideMut<Allocator>(&mut allocator)

  // The thief takes from the top (oldest), the owner pops from the bottom (newest).
  let stolen = thief.join()
  let popped = match move worker.pop() {
    Option<i32>.Some { value } => value
    Option<i32>.None => 0
  }
  if stolen != 1 || popped != 2 {
    return 1
  }
  return 0
}

effect fn failed<E>(error: E) -> i32 {
  drop error
  return 1
}

pub fn main() -> i32 {
  return run Effect.catchAll(program(), failed)
}

Send through a mailbox and an injector#

silk
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.core.effect { Effect }
import silk.core.option { Option }
import silk.concurrency.work_queue { Injector, Mailbox }

effect fn program() -> i32
  ! OutOfMemoryError {
  let mut allocator = Allocator.systemAllocatorProvider()

  // A mailbox: senders on any thread, one receiver.
  let mut receiver = run Mailbox.make<i32>() |> Effect.provideMut<Allocator>(&mut allocator)
  let sender = receiver.sender()
  run sender.send(7) |> Effect.provideMut<Allocator>(&mut allocator)
  let received = match move receiver.receive() {
    Option<i32>.Some { value } => value
    Option<i32>.None => 0
  }

  // An injector: a shared FIFO queue.
  let queue = run Injector<i32>.make() |> Effect.provideMut<Allocator>(&mut allocator)
  run queue.push(1) |> Effect.provideMut<Allocator>(&mut allocator)
  run queue.push(2) |> Effect.provideMut<Allocator>(&mut allocator)
  let first = match move queue.pop() {
    Option<i32>.Some { value } => value
    Option<i32>.None => 0
  }
  if received != 7 || first != 1 {
    return 1
  }
  return 0
}

effect fn outOfMemory(error: OutOfMemoryError) -> i32 {
  return 1
}

pub fn main() -> i32 {
  return run Effect.catchAll(program(), outOfMemory)
}

Import as WorkDeque with import silk.concurrency.work_queue { WorkDeque }.

Public declarations: 8.

WorkDeque#

silk
pub struct WorkDeque

The constructor of work-stealing deques: the work of one thread, which idle threads can steal.

Details#

WorkDeque.make returns the Worker that owns a Chase-Lev deque. The worker pushes and pops at the bottom with no contention. Its Stealers take from the top with one compare-exchange. The ring grows by doubling. Old rings stay allocated until the deque is dropped, because a stealer can still read one. Each push allocates, so it can fail with OutOfMemoryError.

Implementation WorkDeque#

silk
impl WorkDeque

Associated function make#

silk
pub effect fn make<T>() -> Worker<T> ! OutOfMemoryError ? &mut Allocator

Makes an empty deque and returns the worker that owns it.

Worker#

silk
pub struct Worker<T>

The owner of a work-stealing deque. It pushes and pops at the bottom.

Details#

A worker is Transferable when T is, so it can move to the thread that runs the work. Push and pop take it exclusively. Worker.pop returns the newest value.

Steal#

silk
pub union Steal<T>

What one steal attempt found.

Empty#

silk
Empty

The deque was empty.

Retry#

silk
Retry

Another thread took the value that this attempt saw. Try again.

Success#

silk
Success

The oldest value in the deque, now owned by the caller.

Field value#

silk
pub value: T

The stolen value.

Implementation Worker<T>#

silk
impl<T> Worker<T>

Method push#

silk
pub effect<'env> fn push<'env>(self: &mut Self, value: T) -> () ! OutOfMemoryError ? &mut Allocator

Pushes value at the bottom, growing the ring when it is full.

Method pop#

silk
pub fn pop(self: &mut Self) -> Option<T>

Pops the most recently pushed value, or reports none when the deque is empty.

Method stealer#

silk
pub fn stealer(self: &Self) -> Stealer<T>

Returns a stealer for this deque.

Stealer#

silk
pub struct Stealer<T>

A handle that takes the oldest values from a Worker's deque, from any thread.

Details#

A stealer is Transferable and Concurrent when T is Transferable.

Implementation Stealer<T>#

silk
impl<T> Stealer<T>

Method steal#

silk
pub fn steal(self: &Self) -> Steal<T>

Takes the oldest value, or reports that the deque was empty or that the attempt lost a race.

Method clone#

silk
pub fn clone(self: &Self) -> Stealer<T>

Adds another stealer for the same deque.

Injector#

silk
pub struct Injector<T>

A shared FIFO queue that all threads can push to and take from.

Details#

Clones are handles to the same queue. An injector is Transferable and Concurrent when T is Transferable. Each operation holds the lock only to link or unlink one parcel.

Implementation Injector<T>#

silk
impl<T> Injector<T>

Associated function make#

silk
pub effect<'env> fn make<'env>() -> Injector<T> ! OutOfMemoryError ? &mut Allocator

Makes an empty queue.

Method push#

silk
pub effect<'env> fn push<'env>(self: &Self, value: T) -> () ! OutOfMemoryError ? &mut Allocator

Appends value at the back.

Method pop#

silk
pub fn pop(self: &Self) -> Option<T>

Takes the value at the front, or reports none when the queue is empty.

Method clone#

silk
pub fn clone(self: &Self) -> Injector<T>

Adds another handle to the same queue.

Mailbox#

silk
pub struct Mailbox

The constructor of multi-producer, single-consumer mailboxes: many threads that send to one receiver.

Details#

It is the Vyukov queue: a send is one swap and one store, with no lock. Each send allocates, so it can fail with OutOfMemoryError.

Gotchas#

MailboxReceiver.receive can return None for a short time while a send is between its two steps.

Implementation Mailbox#

silk
impl Mailbox

Associated function make#

silk
pub effect fn make<T>() -> MailboxReceiver<T> ! OutOfMemoryError ? &mut Allocator

Makes an empty mailbox and returns its receiver.

MailboxReceiver#

silk
pub struct MailboxReceiver<T>

The one receiving end of a Mailbox.

Details#

A receiver is Transferable when T is. A receive takes it exclusively.

Implementation MailboxReceiver<T>#

silk
impl<T> MailboxReceiver<T>

Method receive#

silk
pub fn receive(self: &mut Self) -> Option<T>

Takes the oldest value sent, or reports none.

Gotchas#

A send that has swapped itself in but not yet linked reports none until it links, so a caller that knows a value is coming retries.

Method sender#

silk
pub fn sender(self: &Self) -> MailboxSender<T>

Returns a sender for this mailbox.

MailboxSender#

silk
pub struct MailboxSender<T>

A sending end of a Mailbox. Any thread can hold one.

Details#

A sender is Transferable and Concurrent when T is Transferable.

Implementation MailboxSender<T>#

silk
impl<T> MailboxSender<T>

Method send#

silk
pub effect<'env> fn send<'env>(self: &Self, value: T) -> () ! OutOfMemoryError ? &mut Allocator

Sends value to the receiver.

Method clone#

silk
pub fn clone(self: &Self) -> MailboxSender<T>

Adds another sender for the same mailbox.

Category: Concurrency.

Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, wasm32-wasip1, wasm32-wasip2.

View the source of this page