Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Share state with pool workers

Scoped threads can borrow an atomic or a mutex, but executor jobs must own their inputs. Shared[T] gives a value several owners. Hand each job its own owner with share():

from std.atomic import Atomic, Ordering
from std.sync import Mutex

class Stats:
    count: i64
    total: i64

def work(hits: Shared[Atomic[u64]], stats: Shared[Mutex[Stats]], value: i64) -> Result[i64, OverflowError[i64]]:
    for _ in range(1000):
        hits.fetch_add(1, Ordering.Relaxed)
        with stats.lock() as data:
            count = data.count + 1
            total = data.total + value
            data.count = count
            data.total = total
    Ok(value)

type PoolJobError = FutureError | SubmitError[()] | PoolError

def main() -> Result[(), AllocError | IoError | PoolJobError | OverflowError[i64]]:
    hits = Shared[Atomic[u64]](Atomic(0))?
    stats = Shared[Mutex[Stats]](Mutex(Stats(0, 0)))?
    with ThreadPoolExecutor(2, 4)? as pool:
        first = pool.submit(work, hits.share(), stats.share(), 3).drop_job()?
        second = pool.submit(work, hits.share(), stats.share(), 4).drop_job()?
        print(first.result()?? + second.result()??)?
    print(hits.load(Ordering.Acquire))?
    with stats.lock() as data:
        print(data.count)?
        print(data.total)?
    Ok(())
7
2000
2000
7000

Shared[T](value) moves the value into one allocation, which can fail. share() returns another owner of the same value without allocating. Each owner is dropped like any other value; the last one to drop, on whichever thread, runs the value’s cleanup and frees it.

The handle lends its content: hits.fetch_add(...) and stats.lock() reach the atomic and the mutex through it, and &*hits borrows the atomic itself. The content is shared, never exclusive, so it changes only through an atomic or a mutex:

class Stats:
    count: i64

def main() -> Result[(), AllocError]:
    stats = Shared(Stats(0))?
    stats.count = 1
    Ok(())
a Shared owner gives shared access only; mutate its content through an Atomic or a Mutex inside it

Handles cannot be compared or printed; read the content instead. Take another owner with share(), not copy(). Copying a value that holds a handle, such as a job’s (str, Shared[Atomic[u64]]) input, shares that handle the same way. A value cannot hold a share of itself through a mutex, since it would never be released: link back to it with an index into a list that owns the values.

The compiler’s lock checks see through handles. Shutting a pool down while holding a lock on a mutex one of its jobs can reach is rejected, as is waiting for such a job’s result. Wait after the lock block ends, as main reads stats only after the pool’s block. Nesting a lock through a handle inside a lock on another Mutex[T] of the same type is rejected too, because two handles can name one mutex. See shared owners for the full rules.