2012-11-15 00:39:56 +01:00
|
|
|
#pragma once
|
|
|
|
|
2016-08-14 02:22:19 +02:00
|
|
|
#include "types.h"
|
2019-07-27 00:34:10 +02:00
|
|
|
#include "util/atomic.hpp"
|
2020-02-28 08:43:37 +01:00
|
|
|
#include "util/shared_cptr.hpp"
|
2016-08-14 02:22:19 +02:00
|
|
|
|
2016-02-01 22:55:43 +01:00
|
|
|
#include <string>
|
|
|
|
#include <memory>
|
2018-09-25 14:21:04 +02:00
|
|
|
#include <string_view>
|
2016-02-01 22:55:43 +01:00
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
#include "mutex.h"
|
2016-09-07 00:38:52 +02:00
|
|
|
#include "cond.h"
|
2018-09-25 14:21:04 +02:00
|
|
|
#include "lockless.h"
|
2016-09-07 00:38:52 +02:00
|
|
|
|
2017-11-23 16:37:08 +01:00
|
|
|
// Report error and call std::abort(), defined in main.cpp
|
|
|
|
[[noreturn]] void report_fatal_error(const std::string&);
|
|
|
|
|
2017-10-21 13:21:37 +02:00
|
|
|
// Hardware core layout
|
|
|
|
enum class native_core_arrangement : u32
|
|
|
|
{
|
|
|
|
undefined,
|
|
|
|
generic,
|
|
|
|
intel_ht,
|
|
|
|
amd_ccx
|
|
|
|
};
|
|
|
|
|
|
|
|
enum class thread_class : u32
|
|
|
|
{
|
|
|
|
general,
|
|
|
|
rsx,
|
|
|
|
spu,
|
|
|
|
ppu
|
|
|
|
};
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
enum class thread_state : u32
|
2018-09-25 14:21:04 +02:00
|
|
|
{
|
|
|
|
created, // Initial state
|
2020-02-28 18:45:27 +01:00
|
|
|
aborting, // The thread has been joined in the destructor or explicitly aborted
|
2018-09-25 14:21:04 +02:00
|
|
|
finished // Final state, always set at the end of thread execution
|
|
|
|
};
|
|
|
|
|
|
|
|
template <class Context>
|
|
|
|
class named_thread;
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
struct result_storage
|
|
|
|
{
|
|
|
|
alignas(T) std::byte data[sizeof(T)];
|
|
|
|
|
|
|
|
static constexpr bool empty = false;
|
|
|
|
|
|
|
|
using type = T;
|
|
|
|
|
|
|
|
T* get()
|
|
|
|
{
|
|
|
|
return reinterpret_cast<T*>(&data);
|
|
|
|
}
|
|
|
|
|
|
|
|
const T* get() const
|
|
|
|
{
|
|
|
|
return reinterpret_cast<const T*>(&data);
|
|
|
|
}
|
2018-10-01 19:05:47 +02:00
|
|
|
|
|
|
|
void destroy() noexcept
|
|
|
|
{
|
|
|
|
get()->~T();
|
|
|
|
}
|
2018-09-25 14:21:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
template <>
|
|
|
|
struct result_storage<void>
|
|
|
|
{
|
|
|
|
static constexpr bool empty = true;
|
|
|
|
|
|
|
|
using type = void;
|
|
|
|
};
|
|
|
|
|
|
|
|
template <class Context, typename... Args>
|
|
|
|
using result_storage_t = result_storage<std::invoke_result_t<Context, Args...>>;
|
|
|
|
|
2019-09-15 14:19:32 +02:00
|
|
|
template <typename T, typename = void>
|
|
|
|
struct thread_thread_name : std::bool_constant<false> {};
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
struct thread_thread_name<T, std::void_t<decltype(named_thread<T>::thread_name)>> : std::bool_constant<true> {};
|
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
// Thread base class
|
|
|
|
class thread_base
|
2015-07-01 00:25:52 +02:00
|
|
|
{
|
2018-09-25 14:21:04 +02:00
|
|
|
// Native thread entry point function type
|
|
|
|
#ifdef _WIN32
|
|
|
|
using native_entry = uint(__stdcall*)(void* arg);
|
|
|
|
#else
|
|
|
|
using native_entry = void*(*)(void* arg);
|
|
|
|
#endif
|
2015-08-21 13:07:31 +02:00
|
|
|
|
2016-09-07 00:38:52 +02:00
|
|
|
// Thread handle (platform-specific)
|
|
|
|
atomic_t<std::uintptr_t> m_thread{0};
|
|
|
|
|
2020-03-03 15:53:27 +01:00
|
|
|
// Thread playtoy, that shouldn't be used
|
2016-09-07 00:38:52 +02:00
|
|
|
atomic_t<u32> m_signal{0};
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
// Thread state
|
|
|
|
atomic_t<thread_state> m_state = thread_state::created;
|
2016-09-07 00:38:52 +02:00
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
// Thread state notification info
|
|
|
|
atomic_t<const void*> m_state_notifier{nullptr};
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
// Thread name
|
2020-02-28 08:43:37 +01:00
|
|
|
stx::atomic_cptr<std::string> m_tname;
|
2015-11-26 09:06:29 +01:00
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
//
|
|
|
|
atomic_t<u64> m_cycles = 0;
|
2018-05-24 21:22:07 +02:00
|
|
|
|
2016-05-13 16:01:48 +02:00
|
|
|
// Start thread
|
2018-09-25 14:21:04 +02:00
|
|
|
void start(native_entry);
|
2015-11-26 09:06:29 +01:00
|
|
|
|
|
|
|
// Called at the thread start
|
2019-09-08 22:27:57 +02:00
|
|
|
void initialize(bool(*wait_cb)(const void*));
|
|
|
|
|
|
|
|
// May be called in destructor
|
|
|
|
void notify_abort() noexcept;
|
2016-04-25 12:49:12 +02:00
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
// Called at the thread end, returns true if needs destruction
|
|
|
|
bool finalize(int) noexcept;
|
2016-04-25 12:49:12 +02:00
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
// Cleanup after possibly deleting the thread instance
|
2018-09-25 14:21:04 +02:00
|
|
|
static void finalize() noexcept;
|
2016-05-13 16:01:48 +02:00
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
friend class thread_ctrl;
|
2012-11-15 00:39:56 +01:00
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
template <class Context>
|
|
|
|
friend class named_thread;
|
2015-11-26 09:06:29 +01:00
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
protected:
|
2018-09-25 14:21:04 +02:00
|
|
|
thread_base(std::string_view name);
|
2018-09-22 21:35:52 +02:00
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
~thread_base();
|
2015-09-26 22:46:04 +02:00
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
public:
|
2018-05-24 21:22:07 +02:00
|
|
|
// Get CPU cycles since last time this function was called. First call returns 0.
|
|
|
|
u64 get_cycles();
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
// Wait for the thread (it does NOT change thread state, and can be called from multiple threads)
|
|
|
|
void join() const;
|
|
|
|
|
2016-09-07 00:38:52 +02:00
|
|
|
// Notify the thread
|
2016-05-13 16:01:48 +02:00
|
|
|
void notify();
|
2018-09-25 14:21:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Collection of global function for current thread
|
|
|
|
class thread_ctrl final
|
|
|
|
{
|
|
|
|
// Current thread
|
|
|
|
static thread_local thread_base* g_tls_this_thread;
|
|
|
|
|
|
|
|
// Target cpu core layout
|
|
|
|
static atomic_t<native_core_arrangement> g_native_core_layout;
|
|
|
|
|
|
|
|
// Internal waiting function, may throw. Infinite value is -1.
|
2019-10-06 12:30:56 +02:00
|
|
|
static void _wait_for(u64 usec, bool alert);
|
2018-09-25 14:21:04 +02:00
|
|
|
|
|
|
|
friend class thread_base;
|
|
|
|
|
2020-02-28 08:43:37 +01:00
|
|
|
// Optimized get_name() for logging
|
|
|
|
static std::string get_name_cached();
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
public:
|
2018-10-01 19:05:47 +02:00
|
|
|
// Get current thread name
|
2020-02-28 08:43:37 +01:00
|
|
|
static std::string get_name()
|
2018-10-01 19:05:47 +02:00
|
|
|
{
|
2020-02-28 08:43:37 +01:00
|
|
|
return *g_tls_this_thread->m_tname.load();
|
2018-10-01 19:05:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get thread name
|
|
|
|
template <typename T>
|
2020-02-28 08:43:37 +01:00
|
|
|
static std::string get_name(const named_thread<T>& thread)
|
2018-10-01 19:05:47 +02:00
|
|
|
{
|
2020-02-28 08:43:37 +01:00
|
|
|
return *static_cast<const thread_base&>(thread).m_tname.load();
|
2018-10-01 19:05:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set current thread name (not recommended)
|
|
|
|
static void set_name(std::string_view name)
|
|
|
|
{
|
2020-02-28 08:43:37 +01:00
|
|
|
g_tls_this_thread->m_tname.store(stx::shared_cptr<std::string>::make(name));
|
2018-10-01 19:05:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set thread name (not recommended)
|
|
|
|
template <typename T>
|
|
|
|
static void set_name(named_thread<T>& thread, std::string_view name)
|
|
|
|
{
|
2020-02-28 08:43:37 +01:00
|
|
|
static_cast<thread_base&>(thread).m_tname.store(stx::shared_cptr<std::string>::make(name));
|
2018-10-01 19:05:47 +02:00
|
|
|
}
|
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
template <typename T>
|
|
|
|
static u64 get_cycles(named_thread<T>& thread)
|
|
|
|
{
|
|
|
|
return static_cast<thread_base&>(thread).get_cycles();
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static void notify(named_thread<T>& thread)
|
|
|
|
{
|
|
|
|
static_cast<thread_base&>(thread).notify();
|
|
|
|
}
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
// Read current state
|
|
|
|
static inline thread_state state()
|
|
|
|
{
|
|
|
|
return g_tls_this_thread->m_state;
|
|
|
|
}
|
2016-05-13 16:01:48 +02:00
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
// Wait once with timeout. May spuriously return false.
|
2019-10-06 12:30:56 +02:00
|
|
|
static inline void wait_for(u64 usec, bool alert = true)
|
2016-05-13 16:01:48 +02:00
|
|
|
{
|
2019-10-06 12:30:56 +02:00
|
|
|
_wait_for(usec, alert);
|
2016-05-13 16:01:48 +02:00
|
|
|
}
|
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
// Wait.
|
2016-09-07 00:38:52 +02:00
|
|
|
static inline void wait()
|
2016-07-27 23:43:22 +02:00
|
|
|
{
|
2019-10-06 12:30:56 +02:00
|
|
|
_wait_for(-1, true);
|
2016-07-27 23:43:22 +02:00
|
|
|
}
|
|
|
|
|
2020-03-08 12:48:06 +01:00
|
|
|
// Exit.
|
|
|
|
[[noreturn]] static void emergency_exit(std::string_view reason);
|
|
|
|
|
2015-11-26 09:06:29 +01:00
|
|
|
// Get current thread (may be nullptr)
|
2018-09-25 14:21:04 +02:00
|
|
|
static thread_base* get_current()
|
2015-11-26 09:06:29 +01:00
|
|
|
{
|
|
|
|
return g_tls_this_thread;
|
|
|
|
}
|
|
|
|
|
2017-10-21 13:21:37 +02:00
|
|
|
// Detect layout
|
|
|
|
static void detect_cpu_layout();
|
|
|
|
|
|
|
|
// Returns a core affinity mask. Set whether to generate the high priority set or not
|
2019-07-20 14:57:23 +02:00
|
|
|
static u64 get_affinity_mask(thread_class group);
|
2017-10-21 13:21:37 +02:00
|
|
|
|
|
|
|
// Sets the native thread priority
|
2017-06-24 17:36:49 +02:00
|
|
|
static void set_native_priority(int priority);
|
2017-10-21 13:21:37 +02:00
|
|
|
|
|
|
|
// Sets the preferred affinity mask for this thread
|
2019-07-20 14:57:23 +02:00
|
|
|
static void set_thread_affinity_mask(u64 mask);
|
2018-09-25 14:21:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Derived from the callable object Context, possibly a lambda
|
|
|
|
template <class Context>
|
2018-10-11 00:17:19 +02:00
|
|
|
class named_thread final : public Context, result_storage_t<Context>, thread_base
|
2018-09-25 14:21:04 +02:00
|
|
|
{
|
|
|
|
using result = result_storage_t<Context>;
|
|
|
|
using thread = thread_base;
|
|
|
|
|
|
|
|
// Type-erased thread entry point
|
|
|
|
#ifdef _WIN32
|
2020-03-09 17:18:39 +01:00
|
|
|
static inline uint __stdcall entry_point(void* arg)
|
2018-09-25 14:21:04 +02:00
|
|
|
#else
|
2020-03-09 17:18:39 +01:00
|
|
|
static inline void* entry_point(void* arg)
|
2018-09-25 14:21:04 +02:00
|
|
|
#endif
|
|
|
|
{
|
2018-10-11 00:17:19 +02:00
|
|
|
const auto _this = static_cast<named_thread*>(static_cast<thread*>(arg));
|
|
|
|
|
|
|
|
// Perform self-cleanup if necessary
|
|
|
|
if (_this->entry_point())
|
|
|
|
{
|
2020-02-28 18:45:27 +01:00
|
|
|
delete _this;
|
2018-10-11 00:17:19 +02:00
|
|
|
}
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
thread::finalize();
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
bool entry_point()
|
2018-09-25 14:21:04 +02:00
|
|
|
{
|
2019-09-08 22:27:57 +02:00
|
|
|
thread::initialize([](const void* data)
|
|
|
|
{
|
|
|
|
const auto _this = thread_ctrl::get_current();
|
|
|
|
|
|
|
|
if (_this->m_state >= thread_state::aborting)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
_this->m_state_notifier.release(data);
|
|
|
|
|
|
|
|
if (!data)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_this->m_state >= thread_state::aborting)
|
|
|
|
{
|
|
|
|
_this->m_state_notifier.release(nullptr);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
});
|
2018-09-25 14:21:04 +02:00
|
|
|
|
|
|
|
if constexpr (result::empty)
|
|
|
|
{
|
|
|
|
// No result
|
|
|
|
Context::operator()();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Construct the result using placement new (copy elision should happen)
|
|
|
|
new (result::get()) typename result::type(Context::operator()());
|
|
|
|
}
|
|
|
|
|
2018-10-11 00:17:19 +02:00
|
|
|
return thread::finalize(0);
|
|
|
|
}
|
|
|
|
|
2018-10-01 19:05:47 +02:00
|
|
|
friend class thread_ctrl;
|
2018-09-25 14:21:04 +02:00
|
|
|
|
2018-10-01 19:05:47 +02:00
|
|
|
public:
|
2019-09-15 14:19:32 +02:00
|
|
|
// Default constructor
|
2020-02-26 19:00:10 +01:00
|
|
|
template <bool Valid = std::is_default_constructible_v<Context> && thread_thread_name<Context>(), typename = std::enable_if_t<Valid>>
|
2019-09-15 14:19:32 +02:00
|
|
|
named_thread()
|
|
|
|
: Context()
|
2020-02-26 19:00:10 +01:00
|
|
|
, thread(Context::thread_name)
|
2019-09-15 14:19:32 +02:00
|
|
|
{
|
|
|
|
thread::start(&named_thread::entry_point);
|
|
|
|
}
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
// Normal forwarding constructor
|
|
|
|
template <typename... Args, typename = std::enable_if_t<std::is_constructible_v<Context, Args&&...>>>
|
|
|
|
named_thread(std::string_view name, Args&&... args)
|
|
|
|
: Context(std::forward<Args>(args)...)
|
|
|
|
, thread(name)
|
|
|
|
{
|
|
|
|
thread::start(&named_thread::entry_point);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lambda constructor, also the implicit deduction guide candidate
|
|
|
|
named_thread(std::string_view name, Context&& f)
|
|
|
|
: Context(std::forward<Context>(f))
|
|
|
|
, thread(name)
|
|
|
|
{
|
|
|
|
thread::start(&named_thread::entry_point);
|
|
|
|
}
|
|
|
|
|
2018-10-01 19:05:47 +02:00
|
|
|
named_thread(const named_thread&) = delete;
|
|
|
|
|
|
|
|
named_thread& operator=(const named_thread&) = delete;
|
|
|
|
|
2018-09-25 14:21:04 +02:00
|
|
|
// Wait for the completion and access result (if not void)
|
|
|
|
[[nodiscard]] decltype(auto) operator()()
|
|
|
|
{
|
|
|
|
thread::join();
|
|
|
|
|
|
|
|
if constexpr (!result::empty)
|
|
|
|
{
|
|
|
|
return *result::get();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wait for the completion and access result (if not void)
|
|
|
|
[[nodiscard]] decltype(auto) operator()() const
|
|
|
|
{
|
|
|
|
thread::join();
|
|
|
|
|
|
|
|
if constexpr (!result::empty)
|
|
|
|
{
|
|
|
|
return *result::get();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Access thread state
|
|
|
|
operator thread_state() const
|
|
|
|
{
|
|
|
|
return thread::m_state.load();
|
|
|
|
}
|
|
|
|
|
2020-02-28 18:45:27 +01:00
|
|
|
// Try to abort by assigning thread_state::aborting (UB if assigning different state)
|
2018-10-01 19:05:47 +02:00
|
|
|
named_thread& operator=(thread_state s)
|
2018-09-25 14:21:04 +02:00
|
|
|
{
|
2020-02-28 18:45:27 +01:00
|
|
|
ASSUME(s == thread_state::aborting);
|
|
|
|
|
|
|
|
if (s == thread_state::aborting && thread::m_state.compare_and_swap_test(thread_state::created, s))
|
2018-09-25 14:21:04 +02:00
|
|
|
{
|
2018-10-11 00:17:19 +02:00
|
|
|
if (s == thread_state::aborting)
|
2018-09-25 14:21:04 +02:00
|
|
|
{
|
2019-09-08 22:27:57 +02:00
|
|
|
thread::notify_abort();
|
|
|
|
}
|
2018-10-01 19:05:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Context type doesn't need virtual destructor
|
|
|
|
~named_thread()
|
|
|
|
{
|
2020-02-28 18:45:27 +01:00
|
|
|
// Assign aborting state forcefully
|
2019-09-08 22:27:57 +02:00
|
|
|
operator=(thread_state::aborting);
|
2018-10-01 19:05:47 +02:00
|
|
|
thread::join();
|
|
|
|
|
|
|
|
if constexpr (!result::empty)
|
|
|
|
{
|
|
|
|
result::destroy();
|
2018-09-25 14:21:04 +02:00
|
|
|
}
|
|
|
|
}
|
2015-11-26 09:06:29 +01:00
|
|
|
};
|
2020-02-29 12:57:41 +01:00
|
|
|
|
|
|
|
// Group of named threads, similar to named_thread
|
|
|
|
template <class Context>
|
|
|
|
class named_thread_group final
|
|
|
|
{
|
|
|
|
using Thread = named_thread<Context>;
|
|
|
|
|
|
|
|
const u32 m_count;
|
|
|
|
|
|
|
|
Thread* m_threads;
|
|
|
|
|
|
|
|
public:
|
|
|
|
// Lambda constructor, also the implicit deduction guide candidate
|
|
|
|
named_thread_group(std::string_view name, u32 count, const Context& f)
|
|
|
|
: m_count(count)
|
|
|
|
, m_threads(nullptr)
|
|
|
|
{
|
|
|
|
if (count == 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_threads = static_cast<Thread*>(::operator new(sizeof(Thread) * m_count, std::align_val_t{alignof(Thread)}));
|
|
|
|
|
|
|
|
// Create all threads
|
|
|
|
for (u32 i = 0; i < m_count; i++)
|
|
|
|
{
|
|
|
|
new (static_cast<void*>(m_threads + i)) Thread(std::string(name) + std::to_string(i + 1), f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
named_thread_group(const named_thread_group&) = delete;
|
|
|
|
|
|
|
|
named_thread_group& operator=(const named_thread_group&) = delete;
|
|
|
|
|
|
|
|
// Wait for completion
|
|
|
|
void join() const
|
|
|
|
{
|
|
|
|
for (u32 i = 0; i < m_count; i++)
|
|
|
|
{
|
|
|
|
std::as_const(*std::launder(m_threads + i))();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Join and access specific thread
|
|
|
|
auto operator[](u32 index) const
|
|
|
|
{
|
|
|
|
return std::as_const(*std::launder(m_threads + index))();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Join and access specific thread
|
|
|
|
auto operator[](u32 index)
|
|
|
|
{
|
|
|
|
return (*std::launder(m_threads + index))();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dumb iterator
|
|
|
|
auto begin()
|
|
|
|
{
|
|
|
|
return std::launder(m_threads);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dumb iterator
|
|
|
|
auto end()
|
|
|
|
{
|
|
|
|
return m_threads + m_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 size() const
|
|
|
|
{
|
|
|
|
return m_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
~named_thread_group()
|
|
|
|
{
|
|
|
|
// Destroy all threads (it should join them)
|
|
|
|
for (u32 i = 0; i < m_count; i++)
|
|
|
|
{
|
|
|
|
std::launder(m_threads + i)->~Thread();
|
|
|
|
}
|
|
|
|
|
|
|
|
::operator delete(static_cast<void*>(m_threads), std::align_val_t{alignof(Thread)});
|
|
|
|
}
|
|
|
|
};
|