ThreadSchedule 2.0.0
Modern C++ thread management library
Loading...
Searching...
No Matches
thread_wrapper.hpp
Go to the documentation of this file.
1#pragma once
2
7
8#include "expected.hpp"
10#include <optional>
11#include <string>
12#include <thread>
13
14#ifdef _WIN32
15# include <libloaderapi.h>
16# include <windows.h>
17#else
18# include <dirent.h>
19# include <fstream>
20# include <sys/prctl.h>
21# include <sys/resource.h>
22# include <sys/syscall.h>
23# include <unistd.h>
24#endif
25
26namespace threadschedule
27{
28
29namespace detail
30{
33{
34};
35
37{
38};
39
40template <typename ThreadType, typename OwnershipTag>
42
58template <typename ThreadType>
59class ThreadStorage<ThreadType, OwningTag>
60{
61 protected:
62 ThreadStorage() = default;
63
64 [[nodiscard]] auto underlying() noexcept -> ThreadType&
65 {
66 return thread_;
67 }
68 [[nodiscard]] auto underlying() const noexcept -> ThreadType const&
69 {
70 return thread_;
71 }
72
73 ThreadType thread_;
74};
75
93template <typename ThreadType>
94class ThreadStorage<ThreadType, NonOwningTag>
95{
96 protected:
97 ThreadStorage() = default;
98 explicit ThreadStorage(ThreadType& t) : external_thread_(&t)
99 {
100 }
101
102 [[nodiscard]] auto underlying() noexcept -> ThreadType&
103 {
104 return *external_thread_;
105 }
106 [[nodiscard]] auto underlying() const noexcept -> ThreadType const&
107 {
108 return *external_thread_;
109 }
110
111 ThreadType* external_thread_ = nullptr; // non-owning
112};
113} // namespace detail
114
167template <typename ThreadType, typename OwnershipTag = detail::OwningTag>
168class BaseThreadWrapper : protected detail::ThreadStorage<ThreadType, OwnershipTag>
169{
170 public:
171 using native_handle_type = typename ThreadType::native_handle_type;
172 using id = typename ThreadType::id;
173
174 BaseThreadWrapper() = default;
175 explicit BaseThreadWrapper(ThreadType& t) : detail::ThreadStorage<ThreadType, OwnershipTag>(t)
176 {
177 }
178 virtual ~BaseThreadWrapper() = default;
179
180 // Thread management
181 void join()
182 {
183 if (underlying().joinable())
184 {
185 underlying().join();
186 }
187 }
188
189 void detach()
190 {
191 if (underlying().joinable())
192 {
193 underlying().detach();
194 }
195 }
196
197 [[nodiscard]] auto joinable() const noexcept -> bool
198 {
199 return underlying().joinable();
200 }
201 [[nodiscard]] auto get_id() const noexcept -> id
202 {
203 return underlying().get_id();
204 }
205 [[nodiscard]] auto native_handle() noexcept -> native_handle_type
206 {
207 return underlying().native_handle();
208 }
209
210 [[nodiscard]] auto set_name(std::string const& name) -> expected<void, std::error_code>
211 {
212 return detail::apply_name(native_handle(), name);
213 }
214
215 [[nodiscard]] auto get_name() const -> std::optional<std::string>
216 {
217 return detail::read_name(const_cast<BaseThreadWrapper*>(this)->native_handle());
218 }
219
221 {
222 return detail::apply_priority(native_handle(), priority);
223 }
224
225 [[nodiscard]] auto set_scheduling_policy(SchedulingPolicy policy, ThreadPriority priority)
227 {
228 return detail::apply_scheduling_policy(native_handle(), policy, priority);
229 }
230
231 [[nodiscard]] auto set_affinity(ThreadAffinity const& affinity) -> expected<void, std::error_code>
232 {
233 return detail::apply_affinity(native_handle(), affinity);
234 }
235
236 [[nodiscard]] auto get_affinity() const -> std::optional<ThreadAffinity>
237 {
238 return detail::read_affinity(const_cast<BaseThreadWrapper*>(this)->native_handle());
239 }
240
241 // Nice value (process-level, affects all threads)
242 static auto set_nice_value(int nice_value) -> bool
243 {
244#ifdef _WIN32
245 // Windows has process priority classes, not nice values
246 // We'll use SetPriorityClass for the process
247 DWORD priority_class;
248 if (nice_value <= -15)
249 {
250 priority_class = HIGH_PRIORITY_CLASS;
251 }
252 else if (nice_value <= -10)
253 {
254 priority_class = ABOVE_NORMAL_PRIORITY_CLASS;
255 }
256 else if (nice_value < 10)
257 {
258 priority_class = NORMAL_PRIORITY_CLASS;
259 }
260 else if (nice_value < 19)
261 {
262 priority_class = BELOW_NORMAL_PRIORITY_CLASS;
263 }
264 else
265 {
266 priority_class = IDLE_PRIORITY_CLASS;
267 }
268 return SetPriorityClass(GetCurrentProcess(), priority_class) != 0;
269#else
270 return setpriority(PRIO_PROCESS, 0, nice_value) == 0;
271#endif
272 }
273
274 static auto get_nice_value() -> std::optional<int>
275 {
276#ifdef _WIN32
277 // Get Windows process priority class and map to nice value
278 DWORD priority_class = GetPriorityClass(GetCurrentProcess());
279 if (priority_class == 0)
280 {
281 return std::nullopt;
282 }
283
284 // Map Windows priority class to nice value
285 switch (priority_class)
286 {
287 case HIGH_PRIORITY_CLASS:
288 return -15;
289 case ABOVE_NORMAL_PRIORITY_CLASS:
290 return -10;
291 case NORMAL_PRIORITY_CLASS:
292 return 0;
293 case BELOW_NORMAL_PRIORITY_CLASS:
294 return 10;
295 case IDLE_PRIORITY_CLASS:
296 return 19;
297 default:
298 return 0;
299 }
300#else
301 errno = 0;
302 int const nice = getpriority(PRIO_PROCESS, 0);
303 if (errno == 0)
304 {
305 return nice;
306 }
307 return std::nullopt;
308#endif
309 }
310
311 protected:
312 using detail::ThreadStorage<ThreadType, OwnershipTag>::underlying;
313 using detail::ThreadStorage<ThreadType, OwnershipTag>::ThreadStorage;
314};
315
352class ThreadWrapper : public BaseThreadWrapper<std::thread, detail::OwningTag>
353{
354 public:
355 ThreadWrapper() = default;
356
357 // Construct by taking ownership of an existing std::thread (move)
358 ThreadWrapper(std::thread&& t) noexcept
359 {
360 this->underlying() = std::move(t);
361 }
362
363 template <typename F, typename... Args>
364 explicit ThreadWrapper(F&& f, Args&&... args) : BaseThreadWrapper()
365 {
366 this->underlying() = std::thread(std::forward<F>(f), std::forward<Args>(args)...);
367 }
368
369 ThreadWrapper(ThreadWrapper const&) = delete;
370 auto operator=(ThreadWrapper const&) -> ThreadWrapper& = delete;
371
372 ThreadWrapper(ThreadWrapper&& other) noexcept
373 {
374 this->underlying() = std::move(other.underlying());
375 }
376
377 auto operator=(ThreadWrapper&& other) noexcept -> ThreadWrapper&
378 {
379 if (this != &other)
380 {
381 if (this->underlying().joinable())
382 {
383 this->underlying().join();
384 }
385 this->underlying() = std::move(other.underlying());
386 }
387 return *this;
388 }
389
390 ~ThreadWrapper() override
391 {
392 if (this->underlying().joinable())
393 {
394 this->underlying().join();
395 }
396 }
397
398 // Ownership transfer to std::thread for APIs that take plain std::thread
399 auto release() noexcept -> std::thread
400 {
401 return std::move(this->underlying());
402 }
403
404 explicit operator std::thread() && noexcept
405 {
406 return std::move(this->underlying());
407 }
408
409 // Factory methods
410 template <typename F, typename... Args>
411 static auto create_with_config(std::string const& name, SchedulingPolicy policy, ThreadPriority priority, F&& f,
412 Args&&... args) -> ThreadWrapper
413 {
414 ThreadWrapper wrapper(std::forward<F>(f), std::forward<Args>(args)...);
415 (void)wrapper.set_name(name);
416 (void)wrapper.set_scheduling_policy(policy, priority);
417 return wrapper;
418 }
419};
420
440class ThreadWrapperView : public BaseThreadWrapper<std::thread, detail::NonOwningTag>
441{
442 public:
443 ThreadWrapperView(std::thread& t) : BaseThreadWrapper<std::thread, detail::NonOwningTag>(t)
444 {
445 }
446
447 // Non-owning access to the underlying std::thread
448 auto get() noexcept -> std::thread&
449 {
450 return this->underlying();
451 }
452 [[nodiscard]] auto get() const noexcept -> std::thread const&
453 {
454 return this->underlying();
455 }
456};
457
495#if __cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
496class JThreadWrapper : public BaseThreadWrapper<std::jthread, detail::OwningTag>
497{
498 public:
499 JThreadWrapper() = default;
500
501 // Construct by taking ownership of an existing std::jthread (move)
502 JThreadWrapper(std::jthread&& t) noexcept : BaseThreadWrapper()
503 {
504 this->underlying() = std::move(t);
505 }
506
507 // Ownership transfer to std::jthread for APIs that take plain std::jthread
508 auto release() noexcept -> std::jthread
509 {
510 return std::move(this->underlying());
511 }
512
513 explicit operator std::jthread() && noexcept
514 {
515 return std::move(this->underlying());
516 }
517
518 template <typename F, typename... Args>
519 explicit JThreadWrapper(F&& f, Args&&... args) : BaseThreadWrapper()
520 {
521 this->underlying() = std::jthread(std::forward<F>(f), std::forward<Args>(args)...);
522 }
523
524 JThreadWrapper(JThreadWrapper const&) = delete;
525 auto operator=(JThreadWrapper const&) -> JThreadWrapper& = delete;
526
527 JThreadWrapper(JThreadWrapper&& other) noexcept
528 {
529 this->underlying() = std::move(other.underlying());
530 }
531
532 auto operator=(JThreadWrapper&& other) noexcept -> JThreadWrapper&
533 {
534 if (this != &other)
535 {
536 this->underlying() = std::move(other.underlying());
537 }
538 return *this;
539 }
540
541 // jthread-specific functionality
542 void request_stop()
543 {
544 this->underlying().request_stop();
545 }
546 [[nodiscard]] auto stop_requested() const noexcept -> bool
547 {
548 return this->underlying().get_stop_token().stop_requested();
549 }
550 [[nodiscard]] auto get_stop_token() const noexcept -> std::stop_token
551 {
552 return this->underlying().get_stop_token();
553 }
554 [[nodiscard]] auto get_stop_source() noexcept -> std::stop_source
555 {
556 return this->underlying().get_stop_source();
557 }
558
559 // Factory methods
560 template <typename F, typename... Args>
561 static auto create_with_config(std::string const& name, SchedulingPolicy policy, ThreadPriority priority, F&& f,
562 Args&&... args) -> JThreadWrapper
563 {
564 JThreadWrapper wrapper(std::forward<F>(f), std::forward<Args>(args)...);
565 (void)wrapper.set_name(name);
566 (void)wrapper.set_scheduling_policy(policy, priority);
567 return wrapper;
568 }
569};
570
595class JThreadWrapperView : public BaseThreadWrapper<std::jthread, detail::NonOwningTag>
596{
597 public:
598 JThreadWrapperView(std::jthread& t) : BaseThreadWrapper<std::jthread, detail::NonOwningTag>(t)
599 {
600 }
601
602 void request_stop()
603 {
604 this->underlying().request_stop();
605 }
606 [[nodiscard]] auto stop_requested() const noexcept -> bool
607 {
608 return this->underlying().get_stop_token().stop_requested();
609 }
610 [[nodiscard]] auto get_stop_token() const noexcept -> std::stop_token
611 {
612 return this->underlying().get_stop_token();
613 }
614 [[nodiscard]] auto get_stop_source() noexcept -> std::stop_source
615 {
616 return this->underlying().get_stop_source();
617 }
618
619 // Non-owning access to the underlying std::jthread
620 auto get() noexcept -> std::jthread&
621 {
622 return this->underlying();
623 }
624 [[nodiscard]] auto get() const noexcept -> std::jthread const&
625 {
626 return this->underlying();
627 }
628};
629#else
630// Fallback for compilers without C++20 support
633#endif // C++20
634
669{
670 public:
671#ifdef _WIN32
672 using native_handle_type = void*; // unsupported placeholder
673#else
674 using native_handle_type = pid_t; // Linux TID
675#endif
676
677 explicit ThreadByNameView(const std::string& name)
678 {
679#ifdef _WIN32
680 // Not supported on Windows in this implementation
681 (void)name;
682#else
683 DIR* dir = opendir("/proc/self/task");
684 if (dir == nullptr)
685 return;
686 struct dirent* entry = nullptr;
687 while ((entry = readdir(dir)) != nullptr)
688 {
689 if (entry->d_name[0] == '.')
690 continue;
691 std::string tid_str(entry->d_name);
692 std::string path = std::string("/proc/self/task/") + tid_str + "/comm";
693 std::ifstream in(path);
694 if (!in)
695 continue;
696 std::string current;
697 std::getline(in, current);
698 if (!current.empty() && current.back() == '\n')
699 current.pop_back();
700 if (current == name)
701 {
702 handle_ = static_cast<pid_t>(std::stoi(tid_str));
703 break;
704 }
705 }
706 closedir(dir);
707#endif
708 }
709
710 [[nodiscard]] auto found() const noexcept -> bool
711 {
712#ifdef _WIN32
713 return false;
714#else
715 return handle_ > 0;
716#endif
717 }
718
719 [[nodiscard]] auto set_name(std::string const& name) const -> expected<void, std::error_code>
720 {
721#ifdef _WIN32
722 return unexpected(std::make_error_code(std::errc::function_not_supported));
723#else
724 if (!found())
725 return unexpected(std::make_error_code(std::errc::no_such_process));
726 if (name.length() > 15)
727 return unexpected(std::make_error_code(std::errc::invalid_argument));
728 std::string path = std::string("/proc/self/task/") + std::to_string(handle_) + "/comm";
729 std::ofstream out(path);
730 if (!out)
731 return unexpected(std::error_code(errno, std::generic_category()));
732 out << name;
733 out.flush();
734 if (!out)
735 return unexpected(std::error_code(errno, std::generic_category()));
736 return {};
737#endif
738 }
739
740 [[nodiscard]] auto get_name() const -> std::optional<std::string>
741 {
742#ifdef _WIN32
743 return std::nullopt;
744#else
745 if (!found())
746 return std::nullopt;
747 std::string path = std::string("/proc/self/task/") + std::to_string(handle_) + "/comm";
748 std::ifstream in(path);
749 if (!in)
750 return std::nullopt;
751 std::string current;
752 std::getline(in, current);
753 if (!current.empty() && current.back() == '\n')
754 current.pop_back();
755 return current;
756#endif
757 }
758
759 [[nodiscard]] auto native_handle() const noexcept -> native_handle_type
760 {
761 return handle_;
762 }
763
764 [[nodiscard]] auto set_priority(ThreadPriority priority) const -> expected<void, std::error_code>
765 {
766#ifdef _WIN32
767 return unexpected(std::make_error_code(std::errc::function_not_supported));
768#else
769 if (!found())
770 return unexpected(std::make_error_code(std::errc::no_such_process));
771 return detail::apply_priority(handle_, priority);
772#endif
773 }
774
775 [[nodiscard]] auto set_scheduling_policy(SchedulingPolicy policy, ThreadPriority priority) const
777 {
778#ifdef _WIN32
779 return unexpected(std::make_error_code(std::errc::function_not_supported));
780#else
781 if (!found())
782 return unexpected(std::make_error_code(std::errc::no_such_process));
783 return detail::apply_scheduling_policy(handle_, policy, priority);
784#endif
785 }
786
787 [[nodiscard]] auto set_affinity(ThreadAffinity const& affinity) const -> expected<void, std::error_code>
788 {
789#ifdef _WIN32
790 return unexpected(std::make_error_code(std::errc::function_not_supported));
791#else
792 if (!found())
793 return unexpected(std::make_error_code(std::errc::no_such_process));
794 return detail::apply_affinity(handle_, affinity);
795#endif
796 }
797
798 private:
799#ifdef _WIN32
800 native_handle_type handle_ = nullptr;
801#else
802 native_handle_type handle_ = 0;
803#endif
804};
805
823{
824 public:
825 static auto hardware_concurrency() -> unsigned int
826 {
827 return std::thread::hardware_concurrency();
828 }
829
830 static auto get_thread_id()
831 {
832#ifdef _WIN32
833 return GetCurrentThreadId();
834#else
835 return static_cast<pid_t>(syscall(SYS_gettid));
836#endif
837 }
838
839 static auto get_current_policy() -> std::optional<SchedulingPolicy>
840 {
841#ifdef _WIN32
842 // Windows doesn't have Linux-style scheduling policies
843 // Return OTHER as a default
845#else
846 const int policy = sched_getscheduler(0);
847 if (policy == -1)
848 {
849 return std::nullopt;
850 }
851 return static_cast<SchedulingPolicy>(policy);
852#endif
853 }
854
855 static auto get_current_priority() -> std::optional<int>
856 {
857#ifdef _WIN32
858 HANDLE thread = GetCurrentThread();
859 int priority = GetThreadPriority(thread);
860 if (priority == THREAD_PRIORITY_ERROR_RETURN)
861 {
862 return std::nullopt;
863 }
864 return priority;
865#else
866 sched_param param;
867 if (sched_getparam(0, &param) == 0)
868 {
869 return param.sched_priority;
870 }
871 return std::nullopt;
872#endif
873 }
874};
875
876} // namespace threadschedule
Polymorphic base providing common thread management operations.
auto native_handle() noexcept -> native_handle_type
static auto get_nice_value() -> std::optional< int >
static auto set_nice_value(int nice_value) -> bool
typename ThreadType::native_handle_type native_handle_type
auto get_affinity() const -> std::optional< ThreadAffinity >
auto get_name() const -> std::optional< std::string >
auto set_scheduling_policy(SchedulingPolicy policy, ThreadPriority priority) -> expected< void, std::error_code >
auto get_id() const noexcept -> id
auto set_name(std::string const &name) -> expected< void, std::error_code >
auto set_priority(ThreadPriority priority) -> expected< void, std::error_code >
virtual ~BaseThreadWrapper()=default
auto joinable() const noexcept -> bool
auto set_affinity(ThreadAffinity const &affinity) -> expected< void, std::error_code >
Manages a set of CPU indices to which a thread may be bound.
auto set_name(std::string const &name) const -> expected< void, std::error_code >
auto set_affinity(ThreadAffinity const &affinity) const -> expected< void, std::error_code >
auto native_handle() const noexcept -> native_handle_type
auto set_priority(ThreadPriority priority) const -> expected< void, std::error_code >
auto set_scheduling_policy(SchedulingPolicy policy, ThreadPriority priority) const -> expected< void, std::error_code >
auto get_name() const -> std::optional< std::string >
ThreadByNameView(const std::string &name)
auto found() const noexcept -> bool
Static utility class providing hardware and scheduling introspection.
static auto get_current_priority() -> std::optional< int >
static auto get_current_policy() -> std::optional< SchedulingPolicy >
static auto hardware_concurrency() -> unsigned int
Value-semantic wrapper for a thread scheduling priority.
Non-owning view over an externally managed std::thread.
auto get() const noexcept -> std::thread const &
auto get() noexcept -> std::thread &
Owning wrapper around std::thread with RAII join-on-destroy semantics.
auto operator=(ThreadWrapper const &) -> ThreadWrapper &=delete
auto release() noexcept -> std::thread
auto operator=(ThreadWrapper &&other) noexcept -> ThreadWrapper &
operator std::thread() &&noexcept
static auto create_with_config(std::string const &name, SchedulingPolicy policy, ThreadPriority priority, F &&f, Args &&... args) -> ThreadWrapper
ThreadWrapper(std::thread &&t) noexcept
ThreadWrapper(ThreadWrapper const &)=delete
ThreadWrapper(ThreadWrapper &&other) noexcept
ThreadWrapper(F &&f, Args &&... args)
auto underlying() const noexcept -> ThreadType const &
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 >
SchedulingPolicy
Enumeration of available thread scheduling policies.
@ OTHER
Standard round-robin time-sharing.
ThreadWrapperView JThreadWrapperView
ThreadWrapper JThreadWrapper
Owning wrapper around std::jthread with cooperative cancellation (C++20).
Scheduling policies, thread priority, and CPU affinity types.
Tag type selecting non-owning (pointer) storage in ThreadStorage.
Tag type selecting owning (value) storage in ThreadStorage.