12#include <condition_variable>
28export namespace work {
31concept task = std::is_nothrow_move_constructible_v<T> &&
32 std::is_nothrow_move_assignable_v<T> && std::is_nothrow_destructible_v<T>;
34template<task T,
class Handler>
class gig;
36template<
class Handler,
class T>
38 (std::is_nothrow_invocable_v<Handler &, T, context<T> &> ||
39 std::is_nothrow_invocable_v<Handler &, T> ||
40 std::is_nothrow_invocable_v<Handler &, context<T> &>);
48inline constexpr unsigned batch_limit = 4096;
50 activation * next =
nullptr;
51 void (*run)(activation *)
noexcept =
nullptr;
52 void const * group =
nullptr;
55 inline static thread_local binding * current =
nullptr;
56 binding * previous = current;
58 explicit binding(
void const * value) noexcept : group(value) { current =
this; }
59 ~binding() noexcept { current = previous; }
63struct donation_clock {
64 using clock = std::chrono::steady_clock;
65 std::minstd_rand random;
66 std::exponential_distribution<double> interval{1.0 / 30'000.0};
67 clock::time_point deadline;
68 explicit donation_clock(
unsigned seed) noexcept
69 : random(seed + 1), deadline(clock::now()) { advance(); }
70 void advance() noexcept {
71 auto const ns = std::chrono::nanoseconds{
72 std::max(std::int64_t{1},
static_cast<std::int64_t
>(interval(random)))};
73 deadline += std::max(clock::duration{1}, std::chrono::duration_cast<clock::duration>(ns));
76template<task T>
struct state;
84 template<task T>
friend struct detail::state;
85 template<task T,
class Handler>
friend class work::gig;
86 std::size_t
const width;
88 std::condition_variable ready;
89 detail::activation * first =
nullptr;
90 detail::activation * last =
nullptr;
91 bool stopping =
false;
92 std::vector<std::jthread> threads;
94 void schedule(detail::activation & job)
noexcept;
95 void notify()
noexcept;
96 [[nodiscard]] detail::activation * take()
noexcept;
97 static void execute(detail::activation * job)
noexcept;
98 void worker()
noexcept;
99 void help(std::atomic<bool>
const & done)
noexcept;
104 pool & operator=(
pool const &) =
delete;
108 [[nodiscard]] hint_inline hint_pure std::size_t
workers() const noexcept {
return width; }
111 return detail::binding::current ? 1 : width;
115 [[nodiscard]]
auto gig(Handler && handler,
gig_options options = {})
noexcept;
121 struct alignas(64) lane : activation {
124 std::vector<T> pending;
125 donation_clock timer;
127 lane(state & s, std::size_t i) noexcept : owner(&s), index(i), timer(
static_cast<unsigned>(i)) {}
130 bool const caller_only;
131 std::vector<std::unique_ptr<lane>> lanes;
133 std::vector<T> incoming;
134 std::size_t active = 0;
136 std::atomic<bool> done{
false};
138 state(pool & p, gig_options options) noexcept
139 : workers(p), caller_only(options.caller_only) {
140 auto const n = caller_only ? 1 : std::min(p.workers(), options.concurrency ? options.concurrency : p.workers());
141 for (std::size_t i = 0; i != n; ++i) lanes.push_back(std::make_unique<lane>(*
this, i));
144 void dispatch() noexcept {
145 if (incoming.empty())
return;
149 auto const now = donation_clock::clock::now();
150 for (
auto & target : lanes) { target->timer.deadline = now; target->timer.advance(); }
152 for (
auto & target : lanes)
if (!target->active) {
153 target->pending.swap(incoming);
154 target->active =
true;
156 if (!caller_only) workers.schedule(*target);
160 void close() noexcept {
162 std::lock_guard lock(mutex);
164 if (!active) done.store(
true, std::memory_order_release);
170 [[nodiscard]]
bool refill(lane & source)
noexcept {
171 pool * wake = &workers;
172 bool completed =
false;
174 std::lock_guard lock(mutex);
175 if (!incoming.empty()) { source.pending.swap(incoming);
return true; }
176 source.active =
false;
178 if (closed && !active) {
179 done.store(
true, std::memory_order_release);
184 if (completed) wake->notify();
187 void yield(lane & source)
noexcept {
189 std::lock_guard lock(mutex);
191 for (
auto & item : incoming) source.pending.push_back(std::move(item));
194 if (!caller_only) workers.schedule(source);
196 void donate(lane & source, std::size_t peer)
noexcept {
197 if (source.pending.size() < 2 || peer == source.index)
return;
198 std::unique_lock lock(mutex, std::try_to_lock);
199 auto & target = *lanes[peer];
200 if (!lock || target.active)
return;
201 auto const half = source.pending.size() / 2;
203 for (std::size_t i = 0; i != half; ++i) target.pending.push_back(std::move(source.pending[i]));
204 source.pending.erase(source.pending.begin(), source.pending.begin() + half);
205 target.active =
true;
207 workers.schedule(target);
209 hint_inline
void poll(lane & source)
noexcept {
210 if (!caller_only && lanes.size() > 1) communicate(source);
212 hint_noinline
void communicate(lane & source)
noexcept {
213 auto & timer = source.timer;
214 while (timer.deadline <= donation_clock::clock::now()) {
215 auto peer = std::uniform_int_distribution<std::size_t>{0, lanes.size() - 2}(timer.random);
216 if (peer >= source.index) ++peer;
217 donate(source, peer);
229 template<task U,
class Handler>
friend class work::gig;
230 using state = detail::state<T>;
232 typename state::lane & local;
233 unsigned remaining = detail::batch_limit;
234 context(state & s,
typename state::lane & l) noexcept : owner(s), local(l) {}
236 context(context
const &) =
delete;
238 hint_inline
void push(T value)
noexcept { local.pending.push_back(std::move(value)); }
240 hint_inline
void poll() noexcept { owner.poll(local); }
244 [[nodiscard]] hint_inline
bool pop(T & result)
noexcept {
245 if (!remaining || local.pending.empty())
return false;
247 result = std::move(local.pending.back());
248 local.pending.pop_back();
253 [[nodiscard]] hint_inline hint_pure std::size_t
index() const noexcept {
return local.index; }
262template<task T,
class Handler>
266 detail::state<T> state;
267 std::mutex caller_join;
268 using lane =
typename detail::state<T>::lane;
270 gig(pool & workers, H && fn,
gig_options options) noexcept
271 : handler(std::forward<H>(fn)), state(workers, options) {
272 for (
auto & local : state.lanes) { local->run = &execute; local->group =
this; }
274 static void execute(detail::activation * raw)
noexcept {
275 auto & local = *
static_cast<lane *
>(raw);
276 auto & self = *
static_cast<gig *
>(
const_cast<void *
>(local.group));
277 auto & state = self.state;
281 if constexpr (!std::is_nothrow_invocable_v<Handler &, T, context<T> &> &&
282 !std::is_nothrow_invocable_v<Handler &, T>) {
283 std::invoke(self.handler, queue);
285 for (
unsigned i = 0; i != detail::batch_limit && !local.pending.empty(); ++i) {
287 T value{std::move(local.pending.back())};
288 local.pending.pop_back();
289 if constexpr (std::is_nothrow_invocable_v<Handler &, T, context<T> &>)
290 std::invoke(self.handler, std::move(value), queue);
291 else std::invoke(self.handler, std::move(value));
297 if (local.pending.empty() && !state.refill(local))
return;
301 gig(gig
const &) =
delete;
302 gig & operator=(gig
const &) =
delete;
305 std::lock_guard lock(state.mutex);
306 if (state.closed)
throw std::logic_error(
"work: push into a closed gig");
307 state.incoming.push_back(std::move(value));
311 void push(std::span<T const> values)
requires std::copy_constructible<T> {
312 std::lock_guard lock(state.mutex);
313 if (state.closed)
throw std::logic_error(
"work: push into a closed gig");
315 std::vector<T> batch(values.begin(), values.end());
316 state.incoming.reserve(state.incoming.size() + batch.size());
317 for (
auto & value : batch) state.incoming.push_back(std::move(value));
321 void close() noexcept { state.close(); }
325 for (
auto * at = detail::binding::current; at; at = at->previous)
326 if (at->group ==
this) std::terminate();
327 std::unique_lock lock(caller_join, std::defer_lock);
330 if (state.caller_only) lock.lock();
332 if (state.caller_only) {
333 while (!state.done.load(std::memory_order_acquire)) pool::execute(state.lanes[0].get());
334 }
else state.workers.help(state.done);
340template<task T,
class Handler>
requires handler_for<std::decay_t<Handler>, T>
347void pool::schedule(detail::activation & job)
noexcept {
349 std::lock_guard lock(mutex);
351 if (last) last->next = &job;
else first = &job;
356void pool::notify() noexcept {
359 std::lock_guard lock(mutex);
362detail::activation * pool::take() noexcept {
365 if (!first) last =
nullptr;
368void pool::execute(detail::activation * job)
noexcept {
369 auto const run = job->run;
370 detail::binding scope{job->group};
373void pool::worker() noexcept {
375 std::unique_lock lock(mutex);
376 ready.wait(lock, [&] {
return stopping || first; });
377 if (stopping)
return;
383void pool::help(std::atomic<bool>
const & done)
noexcept {
384 while (!done.load(std::memory_order_acquire)) {
385 std::unique_lock lock(mutex);
386 ready.wait(lock, [&] {
return first || done.load(std::memory_order_acquire); });
387 if (done.load(std::memory_order_acquire))
return;
394 for (std::size_t i = 1; i != width; ++i) threads.emplace_back([
this] { worker(); });
397 { std::lock_guard lock(mutex); stopping =
true; }
Borrowed owner-local submissions; valid only during its handler call. Children are accepted after clo...
std::size_t index() const noexcept
This activation's lane index, stable for the lifetime of the callback.
bool pop(T &result) noexcept
Pop one task in a context-only batch handler, up to this activation's quantum. False ends this batch,...
void push(T value) noexcept
Push a child on this worker's private stack without synchronization.
void poll() noexcept
Donate older queued work when the accumulated stochastic deadline is due.
A typed, nonmovable lifetime boundary for submitted tasks and descendants. Each accepted task runs ex...
void push(std::span< T const > values)
Submit a copied batch under one admission lock.
~gig() noexcept
Close and join before releasing handler and task storage.
void close() noexcept
Stop external submissions. Already accepted descendants still run.
void push(T value)
Submit external work. Throws logic_error if close() won the race.
void join() noexcept
Close and cooperatively await completion. Repeated joins are harmless.
pool(std::size_t workers=1) noexcept
Create a pool whose width includes the joining caller.
std::size_t workers() const noexcept
Maximum number of lanes in a gig by default.
std::size_t available_workers() const noexcept
Preserve caller-only execution for nested bulk phases.
A handler consumes a task, optionally with a borrowed child-submission context.
Tasks can move between workers without throwing.
Bound a gig's concurrency or keep all its callbacks on the joining caller.
bool caller_only
Work starts in join(), on that caller alone.
std::size_t concurrency
Zero uses the pool's full width.