ThreadSchedule 2.0.0
Modern C++ thread management library
Loading...
Searching...
No Matches
scheduler_policy.hpp
Go to the documentation of this file.
1#pragma once
2
7
8#include "expected.hpp"
9#include <algorithm>
10#include <cstdint>
11#include <optional>
12#include <sstream>
13#include <string>
14#include <system_error>
15#include <vector>
16
17#ifdef _WIN32
18#include <windows.h>
19#else
20#include <pthread.h>
21#include <sched.h>
22#include <sys/resource.h>
23#endif
24
25namespace threadschedule
26{
27// expected/result are provided by expected.hpp
28
61enum class SchedulingPolicy : std::uint_fast8_t
62{
63#ifdef _WIN32
64 // Windows doesn't have the same scheduling policies as Linux
65 // We'll use generic values
66 OTHER = 0,
67 FIFO = 1,
68 RR = 2,
69 BATCH = 3,
70 IDLE = 4
71#else
72 OTHER = SCHED_OTHER,
73 FIFO = SCHED_FIFO,
74 RR = SCHED_RR,
75 BATCH = SCHED_BATCH,
76 IDLE = SCHED_IDLE,
77#ifdef SCHED_DEADLINE
78 DEADLINE = SCHED_DEADLINE
79#endif
80#endif
81};
82
114{
115 public:
116 constexpr explicit ThreadPriority(int priority = 0) : priority_(std::clamp(priority, min_priority, max_priority))
117 {
118 }
119
120 [[nodiscard]] constexpr auto value() const noexcept -> int
121 {
122 return priority_;
123 }
124 [[nodiscard]] constexpr auto is_valid() const noexcept -> bool
125 {
126 return priority_ >= min_priority && priority_ <= max_priority;
127 }
128
129 [[nodiscard]] static constexpr auto lowest() noexcept -> ThreadPriority
130 {
131 return ThreadPriority(min_priority);
132 }
133 [[nodiscard]] static constexpr auto normal() noexcept -> ThreadPriority
134 {
135 return ThreadPriority(0);
136 }
137 [[nodiscard]] static constexpr auto highest() noexcept -> ThreadPriority
138 {
139 return ThreadPriority(max_priority);
140 }
141
142 [[nodiscard]] constexpr auto operator==(ThreadPriority const& other) const noexcept -> bool
143 {
144 return priority_ == other.priority_;
145 }
146 [[nodiscard]] constexpr auto operator!=(ThreadPriority const& other) const noexcept -> bool
147 {
148 return priority_ != other.priority_;
149 }
150 [[nodiscard]] constexpr auto operator<(ThreadPriority const& other) const noexcept -> bool
151 {
152 return priority_ < other.priority_;
153 }
154 [[nodiscard]] constexpr auto operator<=(ThreadPriority const& other) const noexcept -> bool
155 {
156 return priority_ <= other.priority_;
157 }
158 [[nodiscard]] constexpr auto operator>(ThreadPriority const& other) const noexcept -> bool
159 {
160 return priority_ > other.priority_;
161 }
162 [[nodiscard]] constexpr auto operator>=(ThreadPriority const& other) const noexcept -> bool
163 {
164 return priority_ >= other.priority_;
165 }
166
167 [[nodiscard]] auto to_string() const -> std::string
168 {
169 std::ostringstream oss;
170 oss << "ThreadPriority(" << priority_ << ")";
171 return oss.str();
172 }
173
174 private:
175 static constexpr int min_priority = -20;
176 static constexpr int max_priority = 19;
177 int priority_;
178};
179
214{
215 public:
217 {
218#ifdef _WIN32
219 group_ = 0;
220 mask_ = 0;
221#else
222 CPU_ZERO(&cpuset_);
223#endif
224 }
225
226 explicit ThreadAffinity(std::vector<int> const& cpus) : ThreadAffinity()
227 {
228 for (int cpu : cpus)
229 {
230 add_cpu(cpu);
231 }
232 }
233
234 // Adds a CPU index. On Windows, indices >= 64 select group = cpu/64 automatically.
235 void add_cpu(int cpu)
236 {
237#ifdef _WIN32
238 if (cpu < 0)
239 return;
240 WORD g = static_cast<WORD>(cpu / 64);
241 int bit = cpu % 64;
242 if (!has_any())
243 {
244 group_ = g;
245 }
246 if (g != group_)
247 {
248 // Single-group affinity object: ignore CPUs from other groups
249 return;
250 }
251 mask_ |= (static_cast<unsigned long long>(1) << bit);
252#else
253 if (cpu >= 0 && cpu < CPU_SETSIZE)
254 {
255 CPU_SET(cpu, &cpuset_);
256 }
257#endif
258 }
259
260 void remove_cpu(int cpu)
261 {
262#ifdef _WIN32
263 if (cpu < 0)
264 return;
265 WORD g = static_cast<WORD>(cpu / 64);
266 int bit = cpu % 64;
267 if (g == group_)
268 {
269 mask_ &= ~(static_cast<unsigned long long>(1) << bit);
270 }
271#else
272 if (cpu >= 0 && cpu < CPU_SETSIZE)
273 {
274 CPU_CLR(cpu, &cpuset_);
275 }
276#endif
277 }
278
279 [[nodiscard]] auto is_set(int cpu) const -> bool
280 {
281#ifdef _WIN32
282 if (cpu < 0)
283 return false;
284 WORD g = static_cast<WORD>(cpu / 64);
285 int bit = cpu % 64;
286 return g == group_ && (mask_ & (static_cast<unsigned long long>(1) << bit)) != 0;
287#else
288 return cpu >= 0 && cpu < CPU_SETSIZE && CPU_ISSET(cpu, &cpuset_);
289#endif
290 }
291
292 [[nodiscard]] auto has_cpu(int cpu) const -> bool
293 {
294 return is_set(cpu);
295 }
296
297 void clear()
298 {
299#ifdef _WIN32
300 mask_ = 0;
301#else
302 CPU_ZERO(&cpuset_);
303#endif
304 }
305
306 [[nodiscard]] auto get_cpus() const -> std::vector<int>
307 {
308 std::vector<int> cpus;
309#ifdef _WIN32
310 for (int i = 0; i < 64; ++i)
311 {
312 if (mask_ & (static_cast<unsigned long long>(1) << i))
313 {
314 cpus.push_back(static_cast<int>(group_) * 64 + i);
315 }
316 }
317#else
318 for (int i = 0; i < CPU_SETSIZE; ++i)
319 {
320 if (CPU_ISSET(i, &cpuset_))
321 {
322 cpus.push_back(i);
323 }
324 }
325#endif
326 return cpus;
327 }
328
329#ifdef _WIN32
330 [[nodiscard]] unsigned long long get_mask() const
331 {
332 return mask_;
333 }
334 [[nodiscard]] WORD get_group() const
335 {
336 return group_;
337 }
338 [[nodiscard]] bool has_any() const
339 {
340 return mask_ != 0;
341 }
342#else
343 [[nodiscard]] auto native_handle() const -> cpu_set_t const&
344 {
345 return cpuset_;
346 }
347#endif
348
349 [[nodiscard]] auto to_string() const -> std::string
350 {
351 auto cpus = get_cpus();
352 std::ostringstream oss;
353 oss << "ThreadAffinity({";
354 for (size_t i = 0; i < cpus.size(); ++i)
355 {
356 if (i > 0)
357 oss << ", ";
358 oss << cpus[i];
359 }
360 oss << "})";
361 return oss.str();
362 }
363
364 private:
365#ifdef _WIN32
366 WORD group_;
367 unsigned long long mask_;
368#else
369 cpu_set_t cpuset_;
370#endif
371};
372
408{
409 public:
410#ifdef _WIN32
411 // Windows doesn't use sched_param, but we'll define a compatible type
412 struct sched_param_win
413 {
414 int sched_priority;
415 };
416
418 ThreadPriority priority)
419 {
420 sched_param_win param{};
421 // On Windows, priority is directly used
422 param.sched_priority = priority.value();
423 return param;
424 }
425
427 {
428 // Windows thread priorities range from -15 to +15
429 return 30;
430 }
431#else
434 {
435 sched_param param{};
436
437 int const policy_int = static_cast<int>(policy);
438 int const min_prio = sched_get_priority_min(policy_int);
439 int const max_prio = sched_get_priority_max(policy_int);
440
441 if (min_prio == -1 || max_prio == -1)
442 {
443 return unexpected(std::make_error_code(std::errc::invalid_argument));
444 }
445
446 param.sched_priority = std::clamp(priority.value(), min_prio, max_prio);
447 return param;
448 }
449
451 {
452 int const policy_int = static_cast<int>(policy);
453 int const min_prio = sched_get_priority_min(policy_int);
454 int const max_prio = sched_get_priority_max(policy_int);
455
456 if (min_prio == -1 || max_prio == -1)
457 {
458 return unexpected(std::make_error_code(std::errc::invalid_argument));
459 }
460
461 return max_prio - min_prio;
462 }
463#endif
464};
465
469inline auto to_string(SchedulingPolicy policy) -> std::string
470{
471 switch (policy)
472 {
474 return "OTHER";
476 return "FIFO";
478 return "RR";
480 return "BATCH";
482 return "IDLE";
483#if defined(SCHED_DEADLINE) && !defined(_WIN32)
484 case SchedulingPolicy::DEADLINE:
485 return "DEADLINE";
486#endif
487 default:
488 return "UNKNOWN";
489 }
490}
491
492// ---------------------------------------------------------------------------
493// detail:: free functions for thread configuration (priority, policy, affinity)
494//
495// Overloaded by handle type so that every wrapper class can delegate with a
496// single call: detail::apply_priority(handle, priority).
497// ---------------------------------------------------------------------------
498
499namespace detail
500{
501
502#ifdef _WIN32
503
504inline auto map_priority_to_win32(int prio_val) -> int
505{
506 if (prio_val <= -10)
507 return THREAD_PRIORITY_IDLE;
508 if (prio_val <= -5)
509 return THREAD_PRIORITY_LOWEST;
510 if (prio_val < 0)
511 return THREAD_PRIORITY_BELOW_NORMAL;
512 if (prio_val == 0)
513 return THREAD_PRIORITY_NORMAL;
514 if (prio_val <= 5)
515 return THREAD_PRIORITY_ABOVE_NORMAL;
516 if (prio_val <= 10)
517 return THREAD_PRIORITY_HIGHEST;
518 return THREAD_PRIORITY_TIME_CRITICAL;
519}
520
521inline auto apply_priority(HANDLE handle, ThreadPriority priority) -> expected<void, std::error_code>
522{
523 if (!handle)
524 return unexpected(std::make_error_code(std::errc::no_such_process));
525 if (SetThreadPriority(handle, map_priority_to_win32(priority.value())) != 0)
526 return {};
527 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
528}
529
530inline auto apply_scheduling_policy(HANDLE handle, SchedulingPolicy /*policy*/, ThreadPriority priority)
531 -> expected<void, std::error_code>
532{
533 return apply_priority(handle, priority);
534}
535
536inline auto apply_affinity(HANDLE handle, ThreadAffinity const& affinity) -> expected<void, std::error_code>
537{
538 if (!handle)
539 return unexpected(std::make_error_code(std::errc::no_such_process));
540 using SetThreadGroupAffinityFn = BOOL(WINAPI*)(HANDLE, const GROUP_AFFINITY*, PGROUP_AFFINITY);
541 HMODULE hMod = GetModuleHandleW(L"kernel32.dll");
542 if (hMod)
543 {
544 auto set_group_affinity = reinterpret_cast<SetThreadGroupAffinityFn>(
545 reinterpret_cast<void*>(GetProcAddress(hMod, "SetThreadGroupAffinity")));
546 if (set_group_affinity && affinity.has_any())
547 {
548 GROUP_AFFINITY ga{};
549 ga.Mask = static_cast<KAFFINITY>(affinity.get_mask());
550 ga.Group = affinity.get_group();
551 if (set_group_affinity(handle, &ga, nullptr) != 0)
552 return {};
553 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
554 }
555 }
556 DWORD_PTR mask = static_cast<DWORD_PTR>(affinity.get_mask());
557 if (SetThreadAffinityMask(handle, mask) != 0)
558 return {};
559 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
560}
561
562inline auto apply_name(HANDLE handle, std::string const& name) -> expected<void, std::error_code>
563{
564 if (!handle)
565 return unexpected(std::make_error_code(std::errc::no_such_process));
566 using SetThreadDescriptionFn = HRESULT(WINAPI*)(HANDLE, PCWSTR);
567 HMODULE hMod = GetModuleHandleW(L"kernel32.dll");
568 if (!hMod)
569 return unexpected(std::make_error_code(std::errc::function_not_supported));
570 auto set_desc = reinterpret_cast<SetThreadDescriptionFn>(
571 reinterpret_cast<void*>(GetProcAddress(hMod, "SetThreadDescription")));
572 if (!set_desc)
573 return unexpected(std::make_error_code(std::errc::function_not_supported));
574 std::wstring wide(name.begin(), name.end());
575 if (SUCCEEDED(set_desc(handle, wide.c_str())))
576 return {};
577 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
578}
579
580inline auto read_name(HANDLE handle) -> std::optional<std::string>
581{
582 if (!handle)
583 return std::nullopt;
584 using GetThreadDescriptionFn = HRESULT(WINAPI*)(HANDLE, PWSTR*);
585 HMODULE hMod = GetModuleHandleW(L"kernel32.dll");
586 if (!hMod)
587 return std::nullopt;
588 auto get_desc = reinterpret_cast<GetThreadDescriptionFn>(
589 reinterpret_cast<void*>(GetProcAddress(hMod, "GetThreadDescription")));
590 if (!get_desc)
591 return std::nullopt;
592 PWSTR thread_name = nullptr;
593 if (SUCCEEDED(get_desc(handle, &thread_name)) && thread_name)
594 {
595 int size = WideCharToMultiByte(CP_UTF8, 0, thread_name, -1, nullptr, 0, nullptr, nullptr);
596 if (size > 0)
597 {
598 std::string result(size - 1, '\0');
599 WideCharToMultiByte(CP_UTF8, 0, thread_name, -1, &result[0], size, nullptr, nullptr);
600 LocalFree(thread_name);
601 return result;
602 }
603 LocalFree(thread_name);
604 }
605 return std::nullopt;
606}
607
608inline auto read_affinity(HANDLE handle) -> std::optional<ThreadAffinity>
609{
610 if (!handle)
611 return std::nullopt;
612 using GetThreadGroupAffinityFn = BOOL(WINAPI*)(HANDLE, PGROUP_AFFINITY);
613 HMODULE hMod = GetModuleHandleW(L"kernel32.dll");
614 if (!hMod)
615 return std::nullopt;
616 auto get_group_affinity = reinterpret_cast<GetThreadGroupAffinityFn>(
617 reinterpret_cast<void*>(GetProcAddress(hMod, "GetThreadGroupAffinity")));
618 if (!get_group_affinity)
619 return std::nullopt;
620 GROUP_AFFINITY ga{};
621 if (get_group_affinity(handle, &ga) != 0)
622 {
623 ThreadAffinity affinity;
624 for (int i = 0; i < 64; ++i)
625 {
626 if ((ga.Mask & (static_cast<KAFFINITY>(1) << i)) != 0)
627 affinity.add_cpu(static_cast<int>(ga.Group) * 64 + i);
628 }
629 if (affinity.has_any())
630 return affinity;
631 }
632 return std::nullopt;
633}
634
635#else // POSIX
636
637// --- shared implementation for pthread_t and pid_t scheduling ---
638
639template <typename SetSchedFn>
640inline auto apply_sched_params(SchedulingPolicy policy, ThreadPriority priority, SetSchedFn&& set_sched)
642{
643 int const policy_int = static_cast<int>(policy);
644 auto params_result = SchedulerParams::create_for_policy(policy, priority);
645 if (!params_result.has_value())
646 return unexpected(params_result.error());
647 if (set_sched(policy_int, &params_result.value()) == 0)
648 return {};
649 return unexpected(std::error_code(errno, std::generic_category()));
650}
651
652// --- pthread_t overloads (BaseThreadWrapper, ThreadControlBlock, PThreadWrapper) ---
653
654inline auto apply_scheduling_policy(pthread_t handle, SchedulingPolicy policy, ThreadPriority priority)
656{
657 return apply_sched_params(policy, priority,
658 [handle](int p, sched_param* sp) { return pthread_setschedparam(handle, p, sp); });
659}
660
661inline auto apply_priority(pthread_t handle, ThreadPriority priority) -> expected<void, std::error_code>
662{
663 return apply_scheduling_policy(handle, SchedulingPolicy::OTHER, priority);
664}
665
666inline auto apply_affinity(pthread_t handle, ThreadAffinity const& affinity) -> expected<void, std::error_code>
667{
668 if (pthread_setaffinity_np(handle, sizeof(cpu_set_t), &affinity.native_handle()) == 0)
669 return {};
670 return unexpected(std::error_code(errno, std::generic_category()));
671}
672
673inline auto apply_name(pthread_t handle, std::string const& name) -> expected<void, std::error_code>
674{
675 if (name.length() > 15)
676 return unexpected(std::make_error_code(std::errc::invalid_argument));
677 if (pthread_setname_np(handle, name.c_str()) == 0)
678 return {};
679 return unexpected(std::error_code(errno, std::generic_category()));
680}
681
682inline auto read_name(pthread_t handle) -> std::optional<std::string>
683{
684 char name[16];
685 if (pthread_getname_np(handle, name, sizeof(name)) == 0)
686 return std::string(name);
687 return std::nullopt;
688}
689
690inline auto read_affinity(pthread_t handle) -> std::optional<ThreadAffinity>
691{
692 cpu_set_t cpuset;
693 CPU_ZERO(&cpuset);
694 if (pthread_getaffinity_np(handle, sizeof(cpu_set_t), &cpuset) == 0)
695 {
696 std::vector<int> cpus;
697 for (int i = 0; i < CPU_SETSIZE; ++i)
698 {
699 if (CPU_ISSET(i, &cpuset))
700 cpus.push_back(i);
701 }
702 return ThreadAffinity(cpus);
703 }
704 return std::nullopt;
705}
706
707// --- pid_t / TID overloads (ThreadByNameView) ---
708
709inline auto apply_scheduling_policy(pid_t tid, SchedulingPolicy policy, ThreadPriority priority)
711{
712 return apply_sched_params(policy, priority,
713 [tid](int p, sched_param* sp) { return sched_setscheduler(tid, p, sp); });
714}
715
717{
719}
720
721inline auto apply_affinity(pid_t tid, ThreadAffinity const& affinity) -> expected<void, std::error_code>
722{
723 if (sched_setaffinity(tid, sizeof(cpu_set_t), &affinity.native_handle()) == 0)
724 return {};
725 return unexpected(std::error_code(errno, std::generic_category()));
726}
727
728#endif
729
730} // namespace detail
731
732} // namespace threadschedule
Static utility class for constructing OS-native scheduling parameters.
static auto get_priority_range(SchedulingPolicy policy) -> expected< int, std::error_code >
static auto create_for_policy(SchedulingPolicy policy, ThreadPriority priority) -> expected< sched_param, std::error_code >
Manages a set of CPU indices to which a thread may be bound.
auto to_string() const -> std::string
auto get_cpus() const -> std::vector< int >
auto has_cpu(int cpu) const -> bool
auto native_handle() const -> cpu_set_t const &
ThreadAffinity(std::vector< int > const &cpus)
auto is_set(int cpu) const -> bool
Value-semantic wrapper for a thread scheduling priority.
static constexpr auto normal() noexcept -> ThreadPriority
constexpr ThreadPriority(int priority=0)
constexpr auto value() const noexcept -> int
constexpr auto is_valid() const noexcept -> bool
constexpr auto operator==(ThreadPriority const &other) const noexcept -> bool
auto to_string() const -> std::string
constexpr auto operator!=(ThreadPriority const &other) const noexcept -> bool
static constexpr auto highest() noexcept -> ThreadPriority
constexpr auto operator>=(ThreadPriority const &other) const noexcept -> bool
constexpr auto operator>(ThreadPriority const &other) const noexcept -> bool
static constexpr auto lowest() noexcept -> ThreadPriority
constexpr auto operator<=(ThreadPriority const &other) const noexcept -> bool
constexpr auto operator<(ThreadPriority const &other) const noexcept -> bool
A result type that holds either a value of type T or an error of type E.
Definition expected.hpp:215
Exception thrown by expected::value() when the object is in the error state.
Definition expected.hpp:162
Polyfill for std::expected (C++23) for pre-C++23 compilers.
auto apply_affinity(pthread_t handle, ThreadAffinity const &affinity) -> expected< void, std::error_code >
auto apply_priority(pthread_t handle, ThreadPriority priority) -> expected< void, std::error_code >
auto read_name(pthread_t handle) -> std::optional< std::string >
auto apply_scheduling_policy(pthread_t handle, SchedulingPolicy policy, ThreadPriority priority) -> expected< void, std::error_code >
auto apply_name(pthread_t handle, std::string const &name) -> expected< void, std::error_code >
auto read_affinity(pthread_t handle) -> std::optional< ThreadAffinity >
auto apply_sched_params(SchedulingPolicy policy, ThreadPriority priority, SetSchedFn &&set_sched) -> expected< void, std::error_code >
SchedulingPolicy
Enumeration of available thread scheduling policies.
@ OTHER
Standard round-robin time-sharing.
@ BATCH
For batch style execution.
@ IDLE
For very low priority background tasks.
auto to_string(SchedulingPolicy policy) -> std::string
String conversion utilities.