ThreadSchedule 3.0.0
Modern C++ thread management library
Loading...
Searching...
No Matches
lightweight_pool_backend_base.hpp
Go to the documentation of this file.
1#pragma once
2
10#include "../callable/bind.hpp"
11#include "../callable/move_only_function.hpp"
12#include "callbacks.hpp"
13#include "deadline.hpp"
16#include "worker_count.hpp"
17
18#include <atomic>
19#include <chrono>
20#include <condition_variable>
21#include <cstddef>
22#include <future>
23#include <memory>
24#include <mutex>
25#include <optional>
26#include <queue>
27#include <stdexcept>
28#include <string>
29#include <system_error>
30#include <thread>
31#include <type_traits>
32#include <utility>
33#include <vector>
34
36{
37
38// ---------------------------------------------------------------------------
39// lightweight_pool_backend_base
40// ---------------------------------------------------------------------------
41
112template <size_t TaskSize = 64>
114{
115 static_assert(TaskSize >= 2 * sizeof(void*), "TaskSize must hold the operation pointer and heap fallback pointer");
116 static_assert(TaskSize % alignof(void*) == 0, "TaskSize must be a multiple of pointer alignment");
117
118 using queued_task = detail::move_only_function<void(), TaskSize - sizeof(void*)>;
119 static_assert(sizeof(queued_task) == TaskSize, "TaskSize must equal the complete callable storage size");
120
121public:
129 explicit lightweight_pool_backend_base(size_t num_threads = default_worker_count(), bool register_workers = false)
130 : num_threads_(checked_worker_count(num_threads)), register_workers_(register_workers)
131 {
132 workers_.reserve(num_threads_);
133 try
134 {
135 for (size_t i = 0; i < num_threads_; ++i)
136 workers_.emplace_back(&lightweight_pool_backend_base::worker_loop, this, i);
137 startup_.wait(num_threads_);
138 }
139 catch (...)
140 {
141 stop_.store(true, std::memory_order_release);
142 condition_.notify_all();
143 for (auto& worker : workers_)
144 if (worker.joinable())
145 worker.join();
146 throw;
147 }
148 }
149
152
157
160
172 template <typename F, typename... Args>
173 void
174 post(F&& f, Args&&... args)
175 {
176 auto r = try_post(std::forward<F>(f), std::forward<Args>(args)...);
177 if (!r.has_value())
178 throw std::runtime_error("lightweight_pool_backend is shutting down");
179 }
180
187 template <typename F, typename... Args>
188 auto
190 {
191 queued_task task(detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
192 {
193 std::lock_guard<std::mutex> lock(mutex_);
194 if (stop_)
195 return unexpected(std::make_error_code(std::errc::operation_canceled));
196 tasks_.push(std::move(task));
197 }
198 condition_.notify_one();
199 return {};
200 }
201
212 template <typename Iterator>
213 void
214 post_batch(Iterator begin, Iterator end)
215 {
216 auto r = try_post_batch(begin, end);
217 if (!r.has_value())
218 throw std::runtime_error("lightweight_pool_backend is shutting down");
219 }
220
225 template <typename Iterator>
226 auto
227 try_post_batch(Iterator begin, Iterator end) -> expected<void, std::error_code>
228 {
229 std::vector<queued_task> prepared;
230 prepared.reserve(detail::multipass_range_size(begin, end));
231 for (auto it = begin; it != end; ++it)
232 prepared.emplace_back(*it);
233
234 bool enqueued = false;
235 try
236 {
237 std::lock_guard<std::mutex> lock(mutex_);
238 if (stop_)
239 return unexpected(std::make_error_code(std::errc::operation_canceled));
240 for (auto& task : prepared)
241 {
242 tasks_.push(std::move(task));
243 enqueued = true;
244 }
245 }
246 catch (...)
247 {
248 if (enqueued)
249 condition_.notify_all();
250 throw;
251 }
252 condition_.notify_all();
253 return {};
254 }
255
257
260
271 void
273 {
274 if (is_current_worker())
276 std::lock_guard<std::recursive_timed_mutex> shutdown_lock(shutdown_mutex_);
277 if (workers_.empty())
278 return;
279 std::queue<queued_task> discarded;
280 {
281 std::lock_guard<std::mutex> lock(mutex_);
282 if (!stop_)
283 stop_ = true;
284 if (policy == shutdown_policy_backend::drop_pending && !tasks_.empty())
285 tasks_.swap(discarded);
286 }
287 shutdown_completed_all_ = shutdown_completed_all_ && discarded.empty();
288 condition_.notify_all();
289 drain_condition_.notify_all();
290 {
291 std::queue<queued_task> empty;
292 discarded.swap(empty);
293 }
294 for (auto& w : workers_)
295 {
296 if (w.joinable())
297 w.join();
298 }
299 workers_.clear();
300 shutdown_completed_at_ = std::chrono::steady_clock::now();
301 }
302
314 auto
315 shutdown_for(std::chrono::milliseconds timeout) -> bool
316 {
317 if (is_current_worker())
319 auto const deadline = shutdown_deadline_after(timeout);
320 std::unique_lock<std::recursive_timed_mutex> shutdown_lock(shutdown_mutex_, std::defer_lock);
321 if (deadline == std::chrono::steady_clock::time_point::max())
322 shutdown_lock.lock();
323 else if (!shutdown_lock.try_lock_until(deadline))
324 return false;
325 std::unique_lock<std::mutex> lock(mutex_);
326 if (workers_.empty())
327 return shutdown_completed_all_ && shutdown_completed_at_ <= deadline;
328 if (!stop_)
329 stop_ = true;
330 condition_.notify_all();
331 bool const drained = drain_condition_.wait_until(
332 lock, deadline, [this] { return tasks_.empty() && active_tasks_.load(std::memory_order_acquire) == 0; });
333 std::queue<queued_task> discarded;
334 if (!drained)
335 tasks_.swap(discarded);
336 shutdown_completed_all_ = shutdown_completed_all_ && discarded.empty();
337 lock.unlock();
338 condition_.notify_all();
339 drain_condition_.notify_all();
340 {
341 std::queue<queued_task> empty;
342 discarded.swap(empty);
343 }
344 if (deadline == std::chrono::steady_clock::time_point::max())
345 {
346 for (auto& worker : workers_)
347 if (worker.joinable())
348 worker.join();
349 workers_.clear();
350 }
351 if (drained && shutdown_completed_at_ == std::chrono::steady_clock::time_point{})
352 shutdown_completed_at_ = std::chrono::steady_clock::now();
353 return drained && shutdown_completed_all_;
354 }
355
357
360
362 [[nodiscard]] auto
363 size() const noexcept -> size_t
364 {
365 return num_threads_;
366 }
367
368 [[nodiscard]] auto
369 is_current_worker() const noexcept -> bool
370 {
371 return current_pool == this;
372 }
373
375
378
384 auto
388 {
389 return detail::configure_worker_threads(workers_, name_prefix, policy, priority,
390 register_workers_ ? &runtime_registry() : nullptr);
391 }
392
393 auto
395 {
396 return detail::configure_worker_threads(workers_, config, register_workers_ ? &runtime_registry() : nullptr);
397 }
398
400 auto
402 {
403 return detail::set_worker_affinity(workers_, affinity);
404 }
405
407 auto
412
414
415private:
416 size_t num_threads_;
417 bool register_workers_;
419 std::vector<detail::thread_backend> workers_;
420 std::queue<queued_task> tasks_;
421 std::mutex mutex_;
422 std::condition_variable condition_;
423 std::condition_variable drain_condition_;
424 std::recursive_timed_mutex shutdown_mutex_;
425 std::atomic<bool> stop_{ false };
426 bool shutdown_completed_all_{ true };
427 std::chrono::steady_clock::time_point shutdown_completed_at_{};
428 std::atomic<size_t> active_tasks_{ 0 };
429 inline static thread_local lightweight_pool_backend_base* current_pool = nullptr;
430
431 void
432 worker_loop(size_t worker_id)
433 {
434 detail::worker_context_guard<lightweight_pool_backend_base> worker_context(current_pool, this);
435 std::optional<registration_guard_backend> registration;
436 try
437 {
438 if (register_workers_)
439 registration.emplace("light_worker_" + std::to_string(worker_id), "threadschedule.pool");
440 startup_.arrive();
441 }
442 catch (...)
443 {
444 startup_.arrive(std::current_exception());
445 return;
446 }
447 while (true)
448 {
449 queued_task task;
450 {
451 std::unique_lock<std::mutex> lock(mutex_);
452 condition_.wait(lock, [this] { return stop_ || !tasks_.empty(); });
453 if (stop_ && tasks_.empty())
454 return;
455 if (!tasks_.empty())
456 {
457 task = std::move(tasks_.front());
458 tasks_.pop();
459 active_tasks_.fetch_add(1, std::memory_order_relaxed);
460 }
461 else
462 continue;
463 }
464 try
465 {
466 task();
467 }
468 catch (...)
469 {
470 }
471 {
472 std::lock_guard<std::mutex> lock(mutex_);
473 active_tasks_.fetch_sub(1, std::memory_order_relaxed);
474 }
475 drain_condition_.notify_all();
476 }
477 }
478};
479
489
490} // namespace threadschedule::detail
void post_batch(Iterator begin, Iterator end)
Post a range of callables under a single lock acquisition.
void post(F &&f, Args &&... args)
Post a fire-and-forget task (throwing variant).
auto configure_threads(native_thread_config const &config) -> expected< void, std::error_code >
lightweight_pool_backend_base(size_t num_threads=default_worker_count(), bool register_workers=false)
Construct a lightweight pool with num_threads workers.
void shutdown(shutdown_policy_backend policy=shutdown_policy_backend::drain)
Shut the pool down.
auto try_post(F &&f, Args &&... args) -> expected< void, std::error_code >
Post a fire-and-forget task (non-throwing variant).
auto set_affinity(native_thread_affinity const &affinity) -> expected< void, std::error_code >
Pin all workers to the same CPU set.
auto size() const noexcept -> size_t
Number of worker threads.
auto operator=(lightweight_pool_backend_base const &) -> lightweight_pool_backend_base &=delete
auto distribute_across_cpus() -> expected< void, std::error_code >
Pin each worker to a distinct CPU core (round-robin).
auto configure_threads(std::string const &name_prefix, native_scheduling_policy policy=native_scheduling_policy::other, native_thread_priority priority=native_thread_priority::normal()) -> expected< void, std::error_code >
Name, schedule and prioritize all worker threads.
auto shutdown_for(std::chrono::milliseconds timeout) -> bool
Attempt a timed drain.
lightweight_pool_backend_base(lightweight_pool_backend_base const &)=delete
auto try_post_batch(Iterator begin, Iterator end) -> expected< void, std::error_code >
Batch post (non-throwing).
Manages a set of CPU indices to which a thread may be bound.
Definition native.hpp:433
Value-semantic wrapper for a thread scheduling priority.
Definition native.hpp:140
static constexpr auto normal() noexcept -> native_thread_priority
Definition native.hpp:164
Aggregates multiple thread_registry_backend instances into a single queryable view.
auto configure_worker_threads(WorkerRange &workers, std::string const &name_prefix, native_scheduling_policy policy, native_thread_priority priority, thread_registry_backend *registry=nullptr) -> expected< void, std::error_code >
native_scheduling_policy
Enumeration of available thread scheduling policies.
Definition native.hpp:85
@ other
Standard round-robin time-sharing.
auto distribute_workers_across_cpus(WorkerRange &workers) -> expected< void, std::error_code >
auto multipass_range_size(Iterator begin, Iterator end) -> size_t
auto default_worker_count() noexcept -> std::size_t
auto bind_args(F &&function, Args &&... args)
Definition bind.hpp:17
auto checked_worker_count(std::size_t count) -> std::size_t
auto set_worker_affinity(WorkerRange &workers, native_thread_affinity const &affinity) -> expected< void, std::error_code >
auto runtime_registry() -> thread_registry_backend &
auto shutdown_deadline_after(std::chrono::milliseconds timeout) -> std::chrono::steady_clock::time_point
Definition deadline.hpp:11
Internal queued-task shutdown behavior.
Worker identity, CPU selection, and registration helpers.
Worker-thread count configuration for pool types.