diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 0cdb362fe..1f2ab936b 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -71,6 +71,8 @@ add_library(common STATIC common_precompiled_headers.h common_types.h construct.h + error.cpp + error.h file_util.cpp file_util.h hash.h @@ -95,6 +97,7 @@ add_library(common STATIC misc.cpp param_package.cpp param_package.h + polyfill_thread.h precompiled_headers.h quaternion.h ring_buffer.h @@ -119,9 +122,11 @@ add_library(common STATIC thread.cpp thread.h thread_queue_list.h + thread_worker.h threadsafe_queue.h timer.cpp timer.h + unique_function.h vector_math.h web_result.h zstd_compression.cpp diff --git a/src/common/error.cpp b/src/common/error.cpp new file mode 100644 index 000000000..ddb03bd45 --- /dev/null +++ b/src/common/error.cpp @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +#include "common/error.h" + +namespace Common { + +std::string NativeErrorToString(int e) { +#ifdef _WIN32 + LPSTR err_str; + + DWORD res = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, e, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&err_str), 1, nullptr); + if (!res) { + return "(FormatMessageA failed to format error)"; + } + std::string ret(err_str); + LocalFree(err_str); + return ret; +#else + char err_str[255]; +#if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) + // Thread safe (GNU-specific) + const char* str = strerror_r(e, err_str, sizeof(err_str)); + return std::string(str); +#else + // Thread safe (XSI-compliant) + int second_err = strerror_r(e, err_str, sizeof(err_str)); + if (second_err != 0) { + return "(strerror_r failed to format error)"; + } + return std::string(err_str); +#endif // GLIBC etc. +#endif // _WIN32 +} + +std::string GetLastErrorMsg() { +#ifdef _WIN32 + return NativeErrorToString(GetLastError()); +#else + return NativeErrorToString(errno); +#endif +} + +} // namespace Common diff --git a/src/common/error.h b/src/common/error.h new file mode 100644 index 000000000..62a3bd835 --- /dev/null +++ b/src/common/error.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace Common { + +// Generic function to get last error message. +// Call directly after the command or use the error num. +// This function might change the error code. +// Defined in error.cpp. +[[nodiscard]] std::string GetLastErrorMsg(); + +// Like GetLastErrorMsg(), but passing an explicit error code. +// Defined in error.cpp. +[[nodiscard]] std::string NativeErrorToString(int e); + +} // namespace Common diff --git a/src/common/polyfill_thread.h b/src/common/polyfill_thread.h new file mode 100644 index 000000000..5a8d1ce08 --- /dev/null +++ b/src/common/polyfill_thread.h @@ -0,0 +1,323 @@ +// SPDX-FileCopyrightText: 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// +// TODO: remove this file when jthread is supported by all compilation targets +// + +#pragma once + +#include + +#ifdef __cpp_lib_jthread + +#include +#include + +namespace Common { + +template +void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) { + cv.wait(lock, token, std::move(pred)); +} + +} // namespace Common + +#else + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace std { +namespace polyfill { + +using stop_state_callbacks = list>; + +class stop_state { +public: + stop_state() = default; + ~stop_state() = default; + + bool request_stop() { + stop_state_callbacks callbacks; + + { + scoped_lock lk{m_lock}; + + if (m_stop_requested.load()) { + // Already set, nothing to do + return false; + } + + // Set as requested + m_stop_requested = true; + + // Copy callback list + callbacks = m_callbacks; + } + + for (auto callback : callbacks) { + callback(); + } + + return true; + } + + bool stop_requested() const { + return m_stop_requested.load(); + } + + stop_state_callbacks::const_iterator insert_callback(function f) { + stop_state_callbacks::const_iterator ret{}; + bool should_run{}; + + { + scoped_lock lk{m_lock}; + should_run = m_stop_requested.load(); + m_callbacks.push_front(f); + ret = m_callbacks.begin(); + } + + if (should_run) { + f(); + } + + return ret; + } + + void remove_callback(stop_state_callbacks::const_iterator it) { + scoped_lock lk{m_lock}; + m_callbacks.erase(it); + } + +private: + mutex m_lock; + atomic m_stop_requested; + stop_state_callbacks m_callbacks; +}; + +} // namespace polyfill + +class stop_token; +class stop_source; +struct nostopstate_t { + explicit nostopstate_t() = default; +}; +inline constexpr nostopstate_t nostopstate{}; + +template +class stop_callback; + +class stop_token { +public: + stop_token() noexcept = default; + + stop_token(const stop_token&) noexcept = default; + stop_token(stop_token&&) noexcept = default; + stop_token& operator=(const stop_token&) noexcept = default; + stop_token& operator=(stop_token&&) noexcept = default; + ~stop_token() = default; + + void swap(stop_token& other) noexcept { + m_stop_state.swap(other.m_stop_state); + } + + [[nodiscard]] bool stop_requested() const noexcept { + return m_stop_state && m_stop_state->stop_requested(); + } + [[nodiscard]] bool stop_possible() const noexcept { + return m_stop_state != nullptr; + } + +private: + friend class stop_source; + template + friend class stop_callback; + stop_token(shared_ptr stop_state) : m_stop_state(move(stop_state)) {} + +private: + shared_ptr m_stop_state; +}; + +class stop_source { +public: + stop_source() : m_stop_state(make_shared()) {} + explicit stop_source(nostopstate_t) noexcept {} + + stop_source(const stop_source&) noexcept = default; + stop_source(stop_source&&) noexcept = default; + stop_source& operator=(const stop_source&) noexcept = default; + stop_source& operator=(stop_source&&) noexcept = default; + ~stop_source() = default; + void swap(stop_source& other) noexcept { + m_stop_state.swap(other.m_stop_state); + } + + [[nodiscard]] stop_token get_token() const noexcept { + return stop_token(m_stop_state); + } + [[nodiscard]] bool stop_possible() const noexcept { + return m_stop_state != nullptr; + } + [[nodiscard]] bool stop_requested() const noexcept { + return m_stop_state && m_stop_state->stop_requested(); + } + bool request_stop() noexcept { + return m_stop_state && m_stop_state->request_stop(); + } + +private: + friend class jthread; + explicit stop_source(shared_ptr stop_state) + : m_stop_state(move(stop_state)) {} + +private: + shared_ptr m_stop_state; +}; + +template +class stop_callback { + static_assert(is_nothrow_destructible_v); + static_assert(is_invocable_v); + +public: + using callback_type = Callback; + + template + requires constructible_from + explicit stop_callback(const stop_token& st, + C&& cb) noexcept(is_nothrow_constructible_v) + : m_stop_state(st.m_stop_state) { + if (m_stop_state) { + m_callback = m_stop_state->insert_callback(move(cb)); + } + } + template + requires constructible_from + explicit stop_callback(stop_token&& st, + C&& cb) noexcept(is_nothrow_constructible_v) + : m_stop_state(move(st.m_stop_state)) { + if (m_stop_state) { + m_callback = m_stop_state->insert_callback(move(cb)); + } + } + ~stop_callback() { + if (m_stop_state && m_callback) { + m_stop_state->remove_callback(*m_callback); + } + } + + stop_callback(const stop_callback&) = delete; + stop_callback(stop_callback&&) = delete; + stop_callback& operator=(const stop_callback&) = delete; + stop_callback& operator=(stop_callback&&) = delete; + +private: + shared_ptr m_stop_state; + optional m_callback; +}; + +template +stop_callback(stop_token, Callback) -> stop_callback; + +class jthread { +public: + using id = thread::id; + using native_handle_type = thread::native_handle_type; + + jthread() noexcept = default; + + template , jthread>>> + explicit jthread(F&& f, Args&&... args) + : m_stop_state(make_shared()), + m_thread(make_thread(move(f), move(args)...)) {} + + ~jthread() { + if (joinable()) { + request_stop(); + join(); + } + } + + jthread(const jthread&) = delete; + jthread(jthread&&) noexcept = default; + jthread& operator=(const jthread&) = delete; + + jthread& operator=(jthread&& other) noexcept { + m_thread.swap(other.m_thread); + m_stop_state.swap(other.m_stop_state); + return *this; + } + + void swap(jthread& other) noexcept { + m_thread.swap(other.m_thread); + m_stop_state.swap(other.m_stop_state); + } + [[nodiscard]] bool joinable() const noexcept { + return m_thread.joinable(); + } + void join() { + m_thread.join(); + } + void detach() { + m_thread.detach(); + m_stop_state.reset(); + } + + [[nodiscard]] id get_id() const noexcept { + return m_thread.get_id(); + } + [[nodiscard]] native_handle_type native_handle() { + return m_thread.native_handle(); + } + [[nodiscard]] stop_source get_stop_source() noexcept { + return stop_source(m_stop_state); + } + [[nodiscard]] stop_token get_stop_token() const noexcept { + return stop_source(m_stop_state).get_token(); + } + bool request_stop() noexcept { + return get_stop_source().request_stop(); + } + [[nodiscard]] static unsigned int hardware_concurrency() noexcept { + return thread::hardware_concurrency(); + } + +private: + template + thread make_thread(F&& f, Args&&... args) { + if constexpr (is_invocable_v, stop_token, decay_t...>) { + return thread(move(f), get_stop_token(), move(args)...); + } else { + return thread(move(f), move(args)...); + } + } + + shared_ptr m_stop_state; + thread m_thread; +}; + +} // namespace std + +namespace Common { + +template +void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) { + if (token.stop_requested()) { + return; + } + + std::stop_callback callback(token, [&] { cv.notify_all(); }); + cv.wait(lock, [&] { return pred() || token.stop_requested(); }); +} + +} // namespace Common + +#endif diff --git a/src/common/thread.cpp b/src/common/thread.cpp index 394d19345..919e33af9 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -1,8 +1,10 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later -#include "common/common_funcs.h" +#include + +#include "common/error.h" #include "common/logging/log.h" #include "common/thread.h" #ifdef __APPLE__ @@ -20,7 +22,6 @@ #ifndef _WIN32 #include #endif -#include #ifdef __FreeBSD__ #define cpu_set_t cpuset_t @@ -28,6 +29,56 @@ namespace Common { +#ifdef _WIN32 + +void SetCurrentThreadPriority(ThreadPriority new_priority) { + auto handle = GetCurrentThread(); + int windows_priority = 0; + switch (new_priority) { + case ThreadPriority::Low: + windows_priority = THREAD_PRIORITY_BELOW_NORMAL; + break; + case ThreadPriority::Normal: + windows_priority = THREAD_PRIORITY_NORMAL; + break; + case ThreadPriority::High: + windows_priority = THREAD_PRIORITY_ABOVE_NORMAL; + break; + case ThreadPriority::VeryHigh: + windows_priority = THREAD_PRIORITY_HIGHEST; + break; + case ThreadPriority::Critical: + windows_priority = THREAD_PRIORITY_TIME_CRITICAL; + break; + default: + windows_priority = THREAD_PRIORITY_NORMAL; + break; + } + SetThreadPriority(handle, windows_priority); +} + +#else + +void SetCurrentThreadPriority(ThreadPriority new_priority) { + pthread_t this_thread = pthread_self(); + + const auto scheduling_type = SCHED_OTHER; + s32 max_prio = sched_get_priority_max(scheduling_type); + s32 min_prio = sched_get_priority_min(scheduling_type); + u32 level = std::max(static_cast(new_priority) + 1, 4U); + + struct sched_param params; + if (max_prio > min_prio) { + params.sched_priority = min_prio + ((max_prio - min_prio) * level) / 4; + } else { + params.sched_priority = min_prio - ((min_prio - max_prio) * level) / 4; + } + + pthread_setschedparam(this_thread, scheduling_type, ¶ms); +} + +#endif + #ifdef _MSC_VER // Sets the debugger-visible name of the current thread. @@ -47,7 +98,7 @@ void SetCurrentThreadName(const char* name) { info.dwType = 0x1000; info.szName = name; - info.dwThreadID = static_cast(-1); + info.dwThreadID = std::numeric_limits::max(); info.dwFlags = 0; __try { @@ -81,6 +132,12 @@ void SetCurrentThreadName(const char* name) { } #endif +#if defined(_WIN32) +void SetCurrentThreadName(const char* name) { + // Do Nothing on MingW +} +#endif + #endif } // namespace Common diff --git a/src/common/thread.h b/src/common/thread.h index b4881fd44..9bd691900 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -1,6 +1,6 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -10,13 +10,15 @@ #include #include #include +#include "common/common_types.h" +#include "common/polyfill_thread.h" namespace Common { class Event { public: void Set() { - std::lock_guard lk{mutex}; + std::scoped_lock lk{mutex}; if (!is_set) { is_set = true; condvar.notify_one(); @@ -29,8 +31,7 @@ public: is_set = false; } - template - bool WaitFor(const std::chrono::duration& time) { + bool WaitFor(const std::chrono::nanoseconds& time) { std::unique_lock lk{mutex}; if (!condvar.wait_for(lk, time, [this] { return is_set.load(); })) return false; @@ -54,6 +55,10 @@ public: is_set = false; } + [[nodiscard]] bool IsSet() { + return is_set; + } + private: std::condition_variable condvar; std::mutex mutex; @@ -65,7 +70,7 @@ public: explicit Barrier(std::size_t count_) : count(count_) {} /// Blocks until all "count" threads have called Sync() - void Sync() { + bool Sync(std::stop_token token = {}) { std::unique_lock lk{mutex}; const std::size_t current_generation = generation; @@ -73,25 +78,37 @@ public: generation++; waiting = 0; condvar.notify_all(); + return true; } else { - condvar.wait(lk, - [this, current_generation] { return current_generation != generation; }); + CondvarWait(condvar, lk, token, + [this, current_generation] { return current_generation != generation; }); + return !token.stop_requested(); } } - std::size_t Generation() const { - std::unique_lock lk(mutex); + std::size_t Generation() { + std::unique_lock lk{mutex}; return generation; } private: - std::condition_variable condvar; - mutable std::mutex mutex; + std::condition_variable_any condvar; + std::mutex mutex; std::size_t count; std::size_t waiting = 0; std::size_t generation = 0; // Incremented once each time the barrier is used }; +enum class ThreadPriority : u32 { + Low = 0, + Normal = 1, + High = 2, + VeryHigh = 3, + Critical = 4, +}; + +void SetCurrentThreadPriority(ThreadPriority new_priority); + void SetCurrentThreadName(const char* name); } // namespace Common diff --git a/src/common/thread_worker.h b/src/common/thread_worker.h new file mode 100644 index 000000000..260ad44e4 --- /dev/null +++ b/src/common/thread_worker.h @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/polyfill_thread.h" +#include "common/thread.h" +#include "common/unique_function.h" + +namespace Common { + +template +class StatefulThreadWorker { + static constexpr bool with_state = !std::is_same_v; + + struct DummyCallable { + int operator()() const noexcept { + return 0; + } + }; + + using Task = + std::conditional_t, UniqueFunction>; + using StateMaker = std::conditional_t, DummyCallable>; + +public: + explicit StatefulThreadWorker(size_t num_workers, std::string name, StateMaker func = {}) + : workers_queued{num_workers}, thread_name{std::move(name)} { + const auto lambda = [this, func](std::stop_token stop_token) { + Common::SetCurrentThreadName(thread_name.c_str()); + { + [[maybe_unused]] std::conditional_t state{func()}; + while (!stop_token.stop_requested()) { + Task task; + { + std::unique_lock lock{queue_mutex}; + if (requests.empty()) { + wait_condition.notify_all(); + } + Common::CondvarWait(condition, lock, stop_token, + [this] { return !requests.empty(); }); + if (stop_token.stop_requested()) { + break; + } + task = std::move(requests.front()); + requests.pop(); + } + if constexpr (with_state) { + task(&state); + } else { + task(); + } + ++work_done; + } + } + ++workers_stopped; + wait_condition.notify_all(); + }; + threads.reserve(num_workers); + for (size_t i = 0; i < num_workers; ++i) { + threads.emplace_back(lambda); + } + } + + StatefulThreadWorker& operator=(const StatefulThreadWorker&) = delete; + StatefulThreadWorker(const StatefulThreadWorker&) = delete; + + StatefulThreadWorker& operator=(StatefulThreadWorker&&) = delete; + StatefulThreadWorker(StatefulThreadWorker&&) = delete; + + void QueueWork(Task work) { + { + std::unique_lock lock{queue_mutex}; + requests.emplace(std::move(work)); + ++work_scheduled; + } + condition.notify_one(); + } + + void WaitForRequests(std::stop_token stop_token = {}) { + std::stop_callback callback(stop_token, [this] { + for (auto& thread : threads) { + thread.request_stop(); + } + }); + std::unique_lock lock{queue_mutex}; + wait_condition.wait(lock, [this] { + return workers_stopped >= workers_queued || work_done >= work_scheduled; + }); + } + +private: + std::queue requests; + std::mutex queue_mutex; + std::condition_variable_any condition; + std::condition_variable wait_condition; + std::atomic work_scheduled{}; + std::atomic work_done{}; + std::atomic workers_stopped{}; + std::atomic workers_queued{}; + std::string thread_name; + std::vector threads; +}; + +using ThreadWorker = StatefulThreadWorker<>; + +} // namespace Common diff --git a/src/common/unique_function.h b/src/common/unique_function.h new file mode 100644 index 000000000..c15d88349 --- /dev/null +++ b/src/common/unique_function.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +namespace Common { + +/// General purpose function wrapper similar to std::function. +/// Unlike std::function, the captured values don't have to be copyable. +/// This class can be moved but not copied. +template +class UniqueFunction { + class CallableBase { + public: + virtual ~CallableBase() = default; + virtual ResultType operator()(Args&&...) = 0; + }; + + template + class Callable final : public CallableBase { + public: + Callable(Functor&& functor_) : functor{std::move(functor_)} {} + ~Callable() override = default; + + ResultType operator()(Args&&... args) override { + return functor(std::forward(args)...); + } + + private: + Functor functor; + }; + +public: + UniqueFunction() = default; + + template + UniqueFunction(Functor&& functor) + : callable{std::make_unique>(std::move(functor))} {} + + UniqueFunction& operator=(UniqueFunction&& rhs) noexcept = default; + UniqueFunction(UniqueFunction&& rhs) noexcept = default; + + UniqueFunction& operator=(const UniqueFunction&) = delete; + UniqueFunction(const UniqueFunction&) = delete; + + ResultType operator()(Args&&... args) const { + return (*callable)(std::forward(args)...); + } + + explicit operator bool() const noexcept { + return static_cast(callable); + } + +private: + std::unique_ptr callable; +}; + +} // namespace Common diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index fe4894ecb..df470fee1 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -27,31 +27,16 @@ void Scheduler::CommandChunk::ExecuteAll(vk::CommandBuffer cmdbuf) { Scheduler::Scheduler(const Instance& instance, RenderpassCache& renderpass_cache) : instance{instance}, renderpass_cache{renderpass_cache}, master_semaphore{instance}, - command_pool{instance, master_semaphore}, stop_requested{false}, + command_pool{instance, master_semaphore}, use_worker_thread{Settings::values.async_command_recording} { AllocateWorkerCommandBuffers(); if (use_worker_thread) { AcquireNewChunk(); - worker_thread = std::thread([this]() { WorkerThread(); }); + worker_thread = std::jthread([this](std::stop_token token) { WorkerThread(token); }); } } -Scheduler::~Scheduler() { - if (!use_worker_thread) { - return; - } - - stop_requested = true; - - // Push a dummy chunk to unblock the thread - { - std::scoped_lock lock{work_mutex}; - work_queue.push(std::move(chunk)); - } - - work_cv.notify_one(); - worker_thread.join(); -} +Scheduler::~Scheduler() = default; void Scheduler::Flush(vk::Semaphore signal, vk::Semaphore wait) { SubmitExecution(signal, wait); @@ -91,7 +76,7 @@ void Scheduler::DispatchWork() { AcquireNewChunk(); } -void Scheduler::WorkerThread() { +void Scheduler::WorkerThread(std::stop_token stop_token) { do { std::unique_ptr work; bool has_submit{false}; @@ -100,8 +85,8 @@ void Scheduler::WorkerThread() { if (work_queue.empty()) { wait_cv.notify_all(); } - work_cv.wait(lock, [this] { return !work_queue.empty() || stop_requested; }); - if (stop_requested) { + Common::CondvarWait(work_cv, lock, stop_token, [&] { return !work_queue.empty(); }); + if (stop_token.stop_requested()) { continue; } work = std::move(work_queue.front()); @@ -115,7 +100,7 @@ void Scheduler::WorkerThread() { } std::scoped_lock reserve_lock{reserve_mutex}; chunk_reserve.push_back(std::move(work)); - } while (!stop_requested); + } while (!stop_token.stop_requested()); } void Scheduler::AllocateWorkerCommandBuffers() { diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index c56e3d68a..ff636f936 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -6,12 +6,12 @@ #include #include #include -#include #include #include #include "common/alignment.h" #include "common/common_funcs.h" #include "common/common_types.h" +#include "common/polyfill_thread.h" #include "video_core/renderer_vulkan/vk_master_semaphore.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" @@ -183,7 +183,7 @@ private: }; private: - void WorkerThread(); + void WorkerThread(std::stop_token stop_token); void AllocateWorkerCommandBuffers(); @@ -205,8 +205,7 @@ private: std::mutex work_mutex; std::condition_variable_any work_cv; std::condition_variable wait_cv; - std::thread worker_thread; - std::atomic_bool stop_requested; + std::jthread worker_thread; bool use_worker_thread; };