Documentation
¶
Overview ¶
Package workstealpool implements concurrent work stealing for worker pools.
Work-stealing pools exist for a specific shape of problem: recursive divide-and-conquer. The classic example is parallel quicksort or a parallel tree walk. You don't know the full list of work upfront. Each piece of work, when you look at it, discovers more work.
A worker pool consists of multiple worker goroutines. Each worker owns a lock-free deque. Workers execute their own work from the bottom of the deque and steal work from the top of other workers' deques when they run out of local work.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type LFdeque ¶
type LFdeque[T any] struct { // contains filtered or unexported fields }
LFdeque is a lock-free double-ended queue.
- top and bottom are ever-increasing int64 counters; indices into the backing array are always `counter % cap`.
- The invariant is `top <= bottom` and the size is just `bottom - top`.
- Because they only ever increase, there is no ABA problem on the CAS below: a value top once held can never recur later.
- The owner works the bottom end (LIFO: PushBottom/PopBottom).
- Thieves work the top end (FIFO: Steal), racing each other, and resolved by CAS
- Thieves only race for the owner's PopBottom for the very last element,
- This race is resolved by a CAS by both the thief and the owner
func NewLFdeque ¶
func (*LFdeque[T]) PopBottom ¶
PopBottom removes and returns the value at the bottom (owner-only). ok is false if the deque was empty, or if a concurrent thief won the race for the last remaining element.
func (*LFdeque[T]) PushBottom ¶
func (d *LFdeque[T]) PushBottom(v T)
PushBottom adds v to the bottom (owner-only).
func (*LFdeque[T]) PushSliceBottom ¶
func (d *LFdeque[T]) PushSliceBottom(v []T)
PushSliceBottom pushes all the elements of the slice `v` into the owner's queue at bottom (LIFO). A thief calls this to store the values it stole
func (*LFdeque[T]) Steal ¶
Steal removes and returns the value at the top (thief-safe: any number of goroutines may call this concurrently, including concurrently with the owner's PushBottom/PopBottom).
type Task ¶
type Task[T, R any] func(ctx context.Context, workerId int, item T, res chan<- R, spawn func(...T)) error
Task is the unit of work a WorkerPool executes.
ctx should be checked by long-running tasks that want to be interruptible. spawn schedules child items of work onto the calling worker's local deque. It must only be called synchronously, from within this Task invocation.
res is the write-only results channel where leaf results can be emitted.
Returning a non-nil error aborts the pool and records it as the terminal error. T is the input type and R is the result type.
type Worker ¶
type Worker[T any] struct { // contains filtered or unexported fields }
Worker owns a local work-stealing deque.
The worker's normal path is to pop work from the bottom of its deque and push newly spawned work onto the bottom. This keeps the common path local to the worker.
Worker does not know about other workers. The WorkerPool coordinates stealing between workers.
type WorkerPool ¶
type WorkerPool[T, R any] struct { // contains filtered or unexported fields }
WorkerPool manages a collection of workers and schedules work between them.
The pool does not care what T represents. It only moves T between worker deques. execute defines how a worker executes a T.
R is the result type expected from each execute call of the worker
func NewWorkerPool ¶
func NewWorkerPool[T, R any]( ctx context.Context, poolSize, initialWorkerCap, resultBuffSize int, execute Task[T, R], ) *WorkerPool[T, R]
NewWorkerPool creates a pool of poolSize workers, each with its own deque of initial capacity initialWorkerCap.
execute defines the work each worker performs for a given item. See Task for the contract around ctx, spawn, and error handling.
The pool does not start running until Submit is called and workers begin pulling from their deques; there is no separate "Start" step, workers run as soon as they're constructed, watching ctx and their deques.
func (*WorkerPool[T, R]) Run ¶
func (p *WorkerPool[T, R]) Run() <-chan R
Run: Result channel generator. Starts all workers and returns the results channel. The channel closes once every worker has exited, either because there's no work left anywhere or because a task returned an error.
Call Wait afterward (or concurrently, while draining results in another goroutine) to get the terminal error, if any.
func (*WorkerPool[T, R]) StealHalf ¶
func (p *WorkerPool[T, R]) StealHalf(thiefIdx int) (ok bool)
StealHalf attempts to steal work for the given worker from a randomly chosen victim among the other workers in the pool. It tries up to len(workers)-1 distinct victims before giving up.
func (*WorkerPool[T, R]) Submit ¶
func (p *WorkerPool[T, R]) Submit(item T)
Submit adds initial work to the pool. Call before Run. Submit does not itself start any workers.
func (*WorkerPool[T, R]) SubmitN ¶ added in v0.1.3
func (p *WorkerPool[T, R]) SubmitN(items ...T)
SubmitN adds multiple initial work items to the pool. Call before Run.
func (*WorkerPool[T, R]) Wait ¶
func (p *WorkerPool[T, R]) Wait() error
Wait blocks until every worker has exited and returns the first error encountered (nil on normal completion). Safe to call while another goroutine drains the results channel returned by Run, since results only closes once workers have exited too.