139class ScopedThreadPhaseExecutorImpl {
141 static_assert(!CaptureExceptions || has_exceptions,
142 "exception capture requires compiler exception support");
143 static constexpr bool captures_callback_exceptions = CaptureExceptions;
145 explicit ScopedThreadPhaseExecutorImpl(std::size_t worker_count) noexcept
146 : worker_count_(worker_count == 0 ? 1 : worker_count) {}
148 ScopedThreadPhaseExecutorImpl() noexcept
149 : ScopedThreadPhaseExecutorImpl(std::thread::hardware_concurrency()) {}
151 [[nodiscard]]
auto worker_count()
const noexcept -> std::size_t {
152 return worker_count_;
155 template <
typename Fn>
156 [[nodiscard]]
auto for_each_operation(std::size_t first, std::size_t count,
163 const auto thread_count = std::min(worker_count_, count);
164 TESS_DIAG_EVENT_VALUE(queued_scoped_thread_dispatch, thread_count);
165 std::atomic<std::size_t> next_offset = 0;
166 std::vector<PlannedExecutionResult> results(count);
167 std::vector<std::thread> threads;
168 threads.reserve(thread_count);
169 auto&& callback = fn;
171 !has_exceptions || CaptureExceptions ||
173 decltype(callback)&, std::size_t>,
174 "NoThrow executors require noexcept callbacks when exceptions are "
177 constexpr auto no_throw_callback =
178 !CaptureExceptions ||
180 decltype(callback)&, std::size_t>;
182 if constexpr (no_throw_callback) {
183 const auto start_worker = [&] {
184 threads.emplace_back([&] {
186 const auto offset = next_offset.fetch_add(1);
187 if (offset >= count) {
190 results[offset] = callback(first + offset);
194#if TESS_HAS_EXCEPTIONS
196 for (std::size_t worker = 0; worker < thread_count; ++worker) {
200 for (
auto& thread : threads) {
206 for (std::size_t worker = 0; worker < thread_count; ++worker) {
211#if TESS_HAS_EXCEPTIONS
212 std::atomic<bool> cancelled =
false;
213 std::exception_ptr exception;
214 std::mutex exception_mutex;
216 for (std::size_t worker = 0; worker < thread_count; ++worker) {
218 threads.emplace_back([&] {
220 const auto offset = next_offset.fetch_add(1);
221 if (offset >= count ||
222 cancelled.load(std::memory_order_acquire)) {
226 results[offset] = callback(first + offset);
229 const std::scoped_lock lock{exception_mutex};
231 exception = std::current_exception();
234 cancelled.store(
true, std::memory_order_release);
240 for (
auto& thread : threads) {
247 for (
auto& thread : threads) {
252 std::rethrow_exception(exception);
257 if constexpr (no_throw_callback) {
258 for (
auto& thread : threads) {
263 for (
const auto result : results) {
264 if (result.status != PlannedExecutionStatus::Executed) {
272 std::size_t worker_count_ = 1;
337class WorkerPoolPhaseExecutorImpl
338 :
private detail::WorkerPoolExceptionState<CaptureExceptions> {
340 static_assert(!CaptureExceptions || has_exceptions,
341 "exception capture requires compiler exception support");
342 static constexpr bool captures_callback_exceptions = CaptureExceptions;
344 explicit WorkerPoolPhaseExecutorImpl(std::size_t worker_count) {
345 const auto count = worker_count == 0 ? std::size_t{1} : worker_count;
346 workers_.reserve(count);
347#if TESS_HAS_EXCEPTIONS
349 for (std::size_t worker = 0; worker < count; ++worker) {
350 workers_.emplace_back([
this] { run_worker(); });
357 const std::scoped_lock lock{mutex_};
360 work_cv_.notify_all();
361 for (
auto& worker : workers_) {
367 for (std::size_t worker = 0; worker < count; ++worker) {
368 workers_.emplace_back([
this] { run_worker(); });
373 WorkerPoolPhaseExecutorImpl()
374 : WorkerPoolPhaseExecutorImpl(std::thread::hardware_concurrency()) {}
376 WorkerPoolPhaseExecutorImpl(
const WorkerPoolPhaseExecutorImpl&) =
delete;
377 auto operator=(
const WorkerPoolPhaseExecutorImpl&)
378 -> WorkerPoolPhaseExecutorImpl& =
delete;
379 WorkerPoolPhaseExecutorImpl(WorkerPoolPhaseExecutorImpl&&) =
delete;
380 auto operator=(WorkerPoolPhaseExecutorImpl&&)
381 -> WorkerPoolPhaseExecutorImpl& =
delete;
383 ~WorkerPoolPhaseExecutorImpl() {
385 const std::scoped_lock lock{mutex_};
388 work_cv_.notify_all();
389 for (
auto& worker : workers_) {
394 [[nodiscard]]
auto worker_count()
const noexcept -> std::size_t {
395 return workers_.size();
402 void reserve_operations(std::size_t count)
const {
403 const std::scoped_lock lock{mutex_};
404 if (dispatch_active_) {
406 "WorkerPoolPhaseExecutor::reserve_operations called during an "
409 if (results_.size() < count) {
410 results_.resize(count);
414 template <
typename Fn>
415 [[nodiscard]]
auto for_each_operation(std::size_t first, std::size_t count,
419 const std::scoped_lock lock{mutex_};
420 if (dispatch_active_) {
422 "WorkerPoolPhaseExecutor::for_each_operation re-entered during "
423 "an active dispatch");
427 TESS_DIAG_EVENT_VALUE(queued_worker_pool_dispatch,
428 std::min(workers_.size(), count));
430 auto&& callback = fn;
431 using Callback = std::remove_reference_t<
decltype(callback)>;
433 !has_exceptions || CaptureExceptions ||
436 "NoThrow executors require noexcept callbacks when exceptions are "
438 constexpr auto no_throw_callback =
439 !CaptureExceptions ||
442 std::size_t runs = 0;
444#if TESS_HAS_EXCEPTIONS
445 std::exception_ptr exception;
448 const std::scoped_lock lock{mutex_};
450 if (dispatch_active_) {
452 "WorkerPoolPhaseExecutor::for_each_operation re-entered during "
453 "an active dispatch");
455 if (results_.size() < count) {
456 results_.resize(count);
462 dispatch_active_ =
true;
463 job_context_ = &callback;
464 if constexpr (no_throw_callback) {
465 job_invoke_nothrow_ =
468 return (*
static_cast<Callback*
>(context))(index);
471#if TESS_HAS_EXCEPTIONS
472 if constexpr (CaptureExceptions) {
473 this->no_throw_job_ = no_throw_callback;
474 if constexpr (!no_throw_callback) {
475 this->invoke_ = [](
void* context,
477 return (*
static_cast<Callback*
>(context))(index);
487 job_stride_ = std::max<std::size_t>(
488 1, count / (std::max<std::size_t>(1, workers_.size()) * 4));
489 next_offset_.store(0, std::memory_order_relaxed);
490 finished_operations_.store(0, std::memory_order_relaxed);
491#if TESS_HAS_EXCEPTIONS
492 if constexpr (CaptureExceptions) {
493 this->cancelled_.store(
false, std::memory_order_relaxed);
494 this->exception_ =
nullptr;
502 runs = (count + job_stride_ - 1) / job_stride_;
509 if (runs >= workers_.size()) {
510 work_cv_.notify_all();
512 for (std::size_t i = 0; i < runs; ++i) {
513 work_cv_.notify_one();
518 std::unique_lock lock{mutex_};
519 done_cv_.wait(lock, [&] {
520 if constexpr (CaptureExceptions) {
521#if TESS_HAS_EXCEPTIONS
522 return active_workers_ == 0 &&
523 (this->exception_ || finished_operations_.load(
524 std::memory_order_acquire) == count);
529 return active_workers_ == 0 &&
530 finished_operations_.load(std::memory_order_acquire) == count;
533#if TESS_HAS_EXCEPTIONS
534 if constexpr (CaptureExceptions) {
535 exception = this->exception_;
541 for (std::size_t offset = 0; offset < count; ++offset) {
542 if (results_[offset].status != PlannedExecutionStatus::Executed) {
543 result = results_[offset];
548 dispatch_active_ =
false;
551#if TESS_HAS_EXCEPTIONS
552 if constexpr (CaptureExceptions) {
554 std::rethrow_exception(exception);
564 std::uint64_t seen_epoch = 0;
566 std::unique_lock lock{mutex_};
567 work_cv_.wait(lock, [&] {
568 return stop_ || (job_active_ && job_epoch_ != seen_epoch);
573 seen_epoch = job_epoch_;
575 auto*
const context = job_context_;
576 const auto invoke_nothrow = job_invoke_nothrow_;
577#if TESS_HAS_EXCEPTIONS
578 detail::PhaseJobInvoke invoke =
nullptr;
579 auto no_throw_job =
true;
580 if constexpr (CaptureExceptions) {
581 invoke = this->invoke_;
582 no_throw_job = this->no_throw_job_;
585 const auto first = job_first_;
586 const auto count = job_count_;
587 const auto stride = job_stride_;
590 if constexpr (!CaptureExceptions) {
591 run_no_throw_job(context, invoke_nothrow, first, count, stride);
593#if TESS_HAS_EXCEPTIONS
594 if constexpr (CaptureExceptions) {
596 run_no_throw_job(context, invoke_nothrow, first, count, stride);
598 run_catching_job(context, invoke, first, count, stride);
605 if (active_workers_ == 0) {
606 done_cv_.notify_one();
611 void run_no_throw_job(
void* context, detail::NoThrowPhaseJobInvoke invoke,
612 std::size_t first, std::size_t count,
613 std::size_t stride)
const noexcept {
616 next_offset_.fetch_add(stride, std::memory_order_relaxed);
617 if (begin >= count) {
620 const auto end = std::min(begin + stride, count);
621 for (
auto offset = begin; offset < end; ++offset) {
622 results_[offset] = invoke(context, first + offset);
624 finished_operations_.fetch_add(end - begin, std::memory_order_release);
628#if TESS_HAS_EXCEPTIONS
629 void run_catching_job(
void* context, detail::PhaseJobInvoke invoke,
630 std::size_t first, std::size_t count,
631 std::size_t stride)
const {
632 auto cancelled =
false;
633 while (!this->cancelled_.load(std::memory_order_acquire)) {
635 next_offset_.fetch_add(stride, std::memory_order_relaxed);
636 if (begin >= count) {
639 const auto end = std::min(begin + stride, count);
640 auto finished = std::size_t{0};
641 for (
auto offset = begin; offset < end; ++offset) {
642 if (this->cancelled_.load(std::memory_order_acquire)) {
647 results_[offset] = invoke(context, first + offset);
650 this->cancelled_.store(
true, std::memory_order_release);
652 const std::scoped_lock exception_lock{mutex_};
653 if (!this->exception_) {
654 this->exception_ = std::current_exception();
663 finished_operations_.fetch_add(finished, std::memory_order_release);
671 mutable std::mutex mutex_;
672 mutable std::condition_variable work_cv_;
673 mutable std::condition_variable done_cv_;
674 mutable std::vector<PlannedExecutionResult> results_;
681 alignas(128)
mutable std::atomic<std::size_t> next_offset_ = 0;
682 alignas(128)
mutable std::atomic<std::size_t> finished_operations_ = 0;
683 alignas(128)
mutable void* job_context_ =
nullptr;
684 mutable detail::NoThrowPhaseJobInvoke job_invoke_nothrow_ =
nullptr;
685 mutable std::size_t job_first_ = 0;
686 mutable std::size_t job_count_ = 0;
687 mutable std::size_t job_stride_ = 1;
688 mutable std::uint64_t job_epoch_ = 0;
689 mutable std::size_t active_workers_ = 0;
690 mutable bool job_active_ =
false;
691 mutable bool dispatch_active_ =
false;
693 std::vector<std::thread> workers_;