|
work 0.1.0
A gig is a bunch of related work. The pool does the work.
|
A gig is a bunch of related work. The pool does the work.
Make another gig with another task type and handler. They can overlap. The pool keeps its threads; each work::gig<T, Handler> owns its handler and task storage. close() stops external submissions. join() waits for that gig and all its descendants, and makes their writes visible to the caller. It also closes the gig if necessary. Destruction joins, so keep things captured by reference alive until then.
For work that discovers more work, take a context:
Those child pushes go onto a private typed stack. Active workers occasionally donate the older half of their backlog to an idle lane. The intervals are exponentially distributed; deadlines accumulate during execution. Deadlines survive batch boundaries and individual idle lanes; an entirely idle gig starts a fresh timer period when new work arrives. The pool dispatches batches, so its indirect call and scheduler lock aren't paid for every task. There is still synchronization at admission, handoff and completion; this is not a wait-free scheduler.
A handler may run concurrently with itself. It must be noexcept, and tasks must support nonthrowing move construction, move assignment and destruction. Allocation failure inside the scheduler terminates. External submission may throw on allocation, or std::logic_error after close. Racing push() and close() either accepts the task completely or rejects it. A context belongs to one callback on one thread; don't save or share it.
The pool width includes a joining caller: pool{8} creates seven background threads. pool{1} runs when you join. Additional simultaneous joining callers can also help execute work. A gig's concurrency is bounded by its lane count. Joins cooperate, including nested joins of other gigs. Don't join your own gig from its callback, introduce cyclic dependencies, or block waiting for pool work when you could join it. Keep the pool alive until all its gigs are gone. Gigs cannot be copied or moved.
When a batch needs a scope of its own, take just the context. pop() stops at the batch boundary, even if the gig has more work. Returning lets other gigs run.
A long callback can call todo.poll() to donate queued children. It doesn't preempt the callback. For a foreign runtime that requires callbacks on its registered thread, pass {.caller_only = true} to gig; work then starts in join() on that caller. {.concurrency = n} limits a normal gig's lanes.
Build with Clang 23+, CMake 4.4+ and Ninja. The only library dependencies are Hint and the platform threads library.
Use find_package(work CONFIG REQUIRED) and link work::work, or add this repository with CMake's FetchContent. On Windows use clang-cl in an MSVC SDK environment. The installed package includes module sources for consumer BMI generation.
The work-pushing approach comes from Acar, Charguéraud and Rainey's Scheduling Parallel Programs by Work Stealing with Private Deques, particularly its sender-initiated scheduler. This implementation uses its own batch dispatch and completion protocol; it does not inherit the paper's proofs.
The API reference is built and published by CI. To build it locally, install Doxygen 1.18+, Pandoc 3.8+ and Python 3, and configure with -DWORK_BUILD_DOCS=ON, then run cmake --build build --target work-docs. The generated site is in build/site/.
Edward Kmett ekmett@gmail.com