diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index 318f13f7f0..f6f6d869e0 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj +++ b/Builds/VisualStudio/stellar-core.vcxproj @@ -771,6 +771,7 @@ exit /b 0 + @@ -1197,6 +1198,7 @@ exit /b 0 + diff --git a/Builds/VisualStudio/stellar-core.vcxproj.filters b/Builds/VisualStudio/stellar-core.vcxproj.filters index d1cf2654c5..8121ba52e4 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj.filters +++ b/Builds/VisualStudio/stellar-core.vcxproj.filters @@ -492,6 +492,9 @@ util + + util + util @@ -2599,6 +2602,9 @@ main + + util + diff --git a/src/transactions/InvokeHostFunctionOpFrame.cpp b/src/transactions/InvokeHostFunctionOpFrame.cpp index 5fc3cf8d34..ae930f736d 100644 --- a/src/transactions/InvokeHostFunctionOpFrame.cpp +++ b/src/transactions/InvokeHostFunctionOpFrame.cpp @@ -34,6 +34,12 @@ #include #include +#ifdef _WIN32 +#ifdef ERROR +#undef ERROR +#endif +#endif + namespace stellar { namespace diff --git a/src/transactions/test/InvokeHostFunctionTests.cpp b/src/transactions/test/InvokeHostFunctionTests.cpp index 710ca74aae..c481522b6e 100644 --- a/src/transactions/test/InvokeHostFunctionTests.cpp +++ b/src/transactions/test/InvokeHostFunctionTests.cpp @@ -6862,8 +6862,6 @@ TEST_CASE("Soroban delegated signer authentication", "[tx][soroban]") InvokeHostFunctionResultCode::INVOKE_HOST_FUNCTION_TRAPPED); } } - // This test causes stack overflow on Windows, but works fine on Linux. -#ifndef WIN32 SECTION("deep delegate tree") { auto buildDelegateChain = [&](int depth) { @@ -6910,7 +6908,6 @@ TEST_CASE("Soroban delegated signer authentication", "[tx][soroban]") InvokeHostFunctionResultCode::INVOKE_HOST_FUNCTION_TRAPPED); } } -#endif } TEST_CASE("Soroban authorization", "[tx][soroban]") diff --git a/src/util/BatchExecutor.cpp b/src/util/BatchExecutor.cpp index 324efc429b..91a18e1eb1 100644 --- a/src/util/BatchExecutor.cpp +++ b/src/util/BatchExecutor.cpp @@ -160,8 +160,9 @@ BatchExecutor::ensureWorkers(size_t count) { size_t index = mWorkers.size(); uint64_t batchId = mBatchId; - mWorkers.emplace_back( - [this, index, batchId]() { workerLoop(index, batchId); }); + mWorkers.emplace_back(WORKER_STACK_BYTES, [this, index, batchId]() { + workerLoop(index, batchId); + }); pinWorker(index); } } diff --git a/src/util/BatchExecutor.h b/src/util/BatchExecutor.h index 652dfc357c..ce13427765 100644 --- a/src/util/BatchExecutor.h +++ b/src/util/BatchExecutor.h @@ -7,6 +7,7 @@ #include "lib/util/finally.h" #include "util/GlobalChecks.h" #include "util/NonCopyable.h" +#include "util/StackThread.h" #include #include @@ -15,12 +16,12 @@ #include #include #include -#include #include #include namespace stellar { +inline constexpr size_t WORKER_STACK_BYTES = 1 << 23; // 8 MiB // Executes batches of CPU-bound tasks in parallel on a pool of worker threads. // @@ -77,7 +78,7 @@ class BatchExecutor : private NonMovableOrCopyable // with std::condition_variable. std::mutex mMutex; std::condition_variable mCondition; - std::vector mWorkers; + std::vector mWorkers; // All allowed logical CPUs in pinning-preference order. std::vector mPinCpuOrder; // Number of distinct physical cores found in the allowed logical CPUs. diff --git a/src/util/StackThread.cpp b/src/util/StackThread.cpp new file mode 100644 index 0000000000..e2a290e941 --- /dev/null +++ b/src/util/StackThread.cpp @@ -0,0 +1,447 @@ +// Copyright 2020 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "util/StackThread.h" + +#include +#include +#include + +#if defined(_WIN32) +// windows.h first, then: +#include +#include +#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION +#define STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000 +#endif +#else +#include +#include +#endif + +namespace stellar +{ + +namespace detail +{ + +void +setCurrentThreadName(std::string const& name) +{ + if (name.empty()) + { + return; + } +#if defined(_WIN32) + // SetThreadDescription is Windows 10 1607+; resolve dynamically so the + // binary still loads on older systems. + using SetDescFn = HRESULT(WINAPI*)(HANDLE, PCWSTR); + static SetDescFn const setDesc = + reinterpret_cast(reinterpret_cast(::GetProcAddress( + ::GetModuleHandleW(L"kernel32.dll"), "SetThreadDescription"))); + if (setDesc != nullptr) + { + // Thread names are ASCII in practice; widen naively. + std::wstring wide(name.begin(), name.end()); + setDesc(::GetCurrentThread(), wide.c_str()); + } +#elif defined(__APPLE__) + // Hard limit of 64 bytes including the NUL; longer names fail with ERANGE. + std::string const truncated = name.substr(0, 63); + ::pthread_setname_np(truncated.c_str()); +#elif defined(__linux__) + // Hard limit of 16 bytes including the NUL; longer names fail with ERANGE. + std::string const truncated = name.substr(0, 15); + ::pthread_setname_np(::pthread_self(), truncated.c_str()); +#elif defined(__FreeBSD__) || defined(__OpenBSD__) + ::pthread_set_name_np(::pthread_self(), name.c_str()); +#else + (void)name; +#endif +} + +#if !defined(_WIN32) +// pthread_attr_setstacksize requires a multiple of the page size and at least +// PTHREAD_STACK_MIN. On glibc >= 2.34 PTHREAD_STACK_MIN is no longer a +// compile-time constant on every architecture, so prefer sysconf. +std::size_t +roundStackSize(std::size_t bytes) +{ + long const pageRaw = ::sysconf(_SC_PAGESIZE); + std::size_t const page = + (pageRaw > 0) ? static_cast(pageRaw) : 4096u; + + std::size_t minStack = page; +#if defined(_SC_THREAD_STACK_MIN) + long const minRaw = ::sysconf(_SC_THREAD_STACK_MIN); + if (minRaw > 0) + { + minStack = std::max(minStack, static_cast(minRaw)); + } +#endif +#if defined(PTHREAD_STACK_MIN) + minStack = std::max(minStack, static_cast(PTHREAD_STACK_MIN)); +#endif + + bytes = std::max(bytes, minStack); + // Round up to a page multiple, guarding against overflow. + if (bytes > (~static_cast(0)) - (page - 1)) + { + return bytes - (bytes % page); + } + return ((bytes + page - 1) / page) * page; +} + +bool +isNullThread(::pthread_t const& t) noexcept +{ + ::pthread_t nullThread{}; + return std::memcmp(&t, &nullThread, sizeof(t)) == 0; +} +#endif + +// Note: passing a C++-linkage function to pthread_create is formally +// unspecified, but is what every real implementation (and libstdc++/libc++ +// themselves) does. +#if defined(_WIN32) +unsigned __stdcall trampoline(void* raw) +#else +void* +trampoline(void* raw) +#endif +{ + std::unique_ptr payload(static_cast(raw)); + try + { + setCurrentThreadName(payload->mName); + payload->run(); + } + catch (...) + { + // std::thread's contract: an exception escaping the thread function + // calls std::terminate. Letting it unwind into the C runtime here + // would be undefined behaviour, so make it explicit. + std::terminate(); + } +#if defined(_WIN32) + return 0u; +#else + return nullptr; +#endif +} + +} // namespace detail + +StackThread::id::id() noexcept : mNative{}, mValid(false) +{ +} + +StackThread::id::id(native_id_type native) noexcept + : mNative(native), mValid(true) +{ +} + +bool +operator==(StackThread::id const& a, StackThread::id const& b) noexcept +{ + if (a.mValid != b.mValid) + { + return false; + } + if (!a.mValid) + { + return true; + } +#if defined(_WIN32) + return a.mNative == b.mNative; +#else + return ::pthread_equal(a.mNative, b.mNative) != 0; +#endif +} + +bool +operator!=(StackThread::id const& a, StackThread::id const& b) noexcept +{ + return !(a == b); +} + +bool +operator<(StackThread::id const& a, StackThread::id const& b) noexcept +{ + if (a.mValid != b.mValid) + { + return !a.mValid; + } + if (!a.mValid) + { + return false; + } + return std::memcmp(&a.mNative, &b.mNative, + sizeof(StackThread::native_id_type)) < 0; +} + +bool +operator>(StackThread::id const& a, StackThread::id const& b) noexcept +{ + return b < a; +} + +bool +operator<=(StackThread::id const& a, StackThread::id const& b) noexcept +{ + return !(b < a); +} + +bool +operator>=(StackThread::id const& a, StackThread::id const& b) noexcept +{ + return !(a < b); +} + +std::size_t +StackThread::id::hash() const noexcept +{ + if (!mValid) + { + return 0; + } + std::uintptr_t scalar = 0; + std::memcpy(&scalar, &mNative, + std::min(sizeof(scalar), sizeof(native_id_type))); + return std::hash{}(scalar); +} + +StackThread::StackThread(StackThread&& other) noexcept +{ + swap(other); +} + +StackThread& +StackThread::operator=(StackThread&& other) noexcept +{ + if (this != &other) + { + if (joinable()) + { + // Matching std::thread: assigning over a joinable thread is + // a programming error, not a silent detach. + std::terminate(); + } + swap(other); + } + return *this; +} + +StackThread::~StackThread() +{ + if (joinable()) + { + std::terminate(); + } +} + +bool +StackThread::joinable() const noexcept +{ +#if defined(_WIN32) + return mHandle != nullptr; +#else + return !detail::isNullThread(mThread); +#endif +} + +void +StackThread::join() +{ + if (!joinable()) + { + throw std::system_error( + std::make_error_code(std::errc::invalid_argument), + "StackThread::join on a non-joinable thread"); + } +#if defined(_WIN32) + if (::GetThreadId(mHandle) == ::GetCurrentThreadId()) + { + throw std::system_error( + std::make_error_code(std::errc::resource_deadlock_would_occur), + "StackThread::join on itself"); + } + DWORD const rc = ::WaitForSingleObject(mHandle, INFINITE); + if (rc != WAIT_OBJECT_0) + { + throw std::system_error(static_cast(::GetLastError()), + std::system_category(), "WaitForSingleObject"); + } + ::CloseHandle(mHandle); + mHandle = nullptr; +#else + int const rc = ::pthread_join(mThread, nullptr); + if (rc != 0) + { + throw std::system_error(rc, std::system_category(), "pthread_join"); + } + mThread = native_handle_type{}; +#endif +} + +void +StackThread::detach() +{ + if (!joinable()) + { + throw std::system_error( + std::make_error_code(std::errc::invalid_argument), + "StackThread::detach on a non-joinable thread"); + } +#if defined(_WIN32) + ::CloseHandle(mHandle); + mHandle = nullptr; +#else + int const rc = ::pthread_detach(mThread); + if (rc != 0) + { + throw std::system_error(rc, std::system_category(), "pthread_detach"); + } + mThread = native_handle_type{}; +#endif +} + +StackThread::id +StackThread::get_id() const noexcept +{ + if (!joinable()) + { + return id{}; + } +#if defined(_WIN32) + return id{::GetThreadId(mHandle)}; +#else + return id{mThread}; +#endif +} + +StackThread::native_handle_type +StackThread::native_handle() const noexcept +{ +#if defined(_WIN32) + return mHandle; +#else + return mThread; +#endif +} + +void +StackThread::swap(StackThread& other) noexcept +{ +#if defined(_WIN32) + std::swap(mHandle, other.mHandle); +#else + std::swap(mThread, other.mThread); +#endif +} + +unsigned int +StackThread::hardware_concurrency() noexcept +{ + return std::thread::hardware_concurrency(); +} + +void +StackThread::setCurrentName(std::string const& name) +{ + detail::setCurrentThreadName(name); +} + +void +StackThread::start(std::size_t stackBytes, + std::unique_ptr payload) +{ +#if defined(_WIN32) + if (stackBytes > static_cast(UINT_MAX)) + { + throw std::system_error( + std::make_error_code(std::errc::invalid_argument), + "requested stack size exceeds the Win32 unsigned limit"); + } + // Without STACK_SIZE_PARAM_IS_A_RESERVATION the size argument is the + // initial *commit*, and the reserve still comes from the PE header. + // _beginthreadex may start executing the trampoline before it returns, so + // create the thread suspended until mHandle is published. This preserves + // std::thread-like constructor synchronization for callables that capture + // the StackThread object under construction. + uintptr_t const h = ::_beginthreadex( + nullptr, static_cast(stackBytes), &detail::trampoline, + payload.get(), STACK_SIZE_PARAM_IS_A_RESERVATION | CREATE_SUSPENDED, + nullptr); + if (h == 0) + { + throw std::system_error(errno, std::generic_category(), + "_beginthreadex"); + } + mHandle = reinterpret_cast(h); + [[maybe_unused]] auto* transferredPayload = payload.release(); + if (::ResumeThread(mHandle) == static_cast(-1)) + { + DWORD const ec = ::GetLastError(); + ::CloseHandle(mHandle); + mHandle = nullptr; + throw std::system_error(static_cast(ec), std::system_category(), + "ResumeThread"); + } +#else + ::pthread_attr_t attr; + int rc = ::pthread_attr_init(&attr); + if (rc != 0) + { + throw std::system_error(rc, std::system_category(), + "pthread_attr_init"); + } + struct AttrGuard + { + ::pthread_attr_t* a; + ~AttrGuard() + { + ::pthread_attr_destroy(a); + } + } guard{&attr}; + + if (stackBytes != 0) + { + rc = ::pthread_attr_setstacksize(&attr, + detail::roundStackSize(stackBytes)); + if (rc != 0) + { + throw std::system_error(rc, std::system_category(), + "pthread_attr_setstacksize"); + } + } + + rc = ::pthread_create(&mThread, &attr, &detail::trampoline, payload.get()); + if (rc != 0) + { + throw std::system_error(rc, std::system_category(), "pthread_create"); + } + [[maybe_unused]] auto* transferredPayload = payload.release(); +#endif +} + +void +swap(StackThread& a, StackThread& b) noexcept +{ + a.swap(b); +} + +} // namespace stellar + +namespace std +{ + +std::size_t +hash<::stellar::StackThread::id>::operator()( + ::stellar::StackThread::id const& v) const noexcept +{ + return v.hash(); +} + +} // namespace std \ No newline at end of file diff --git a/src/util/StackThread.h b/src/util/StackThread.h new file mode 100644 index 0000000000..2764dad4de --- /dev/null +++ b/src/util/StackThread.h @@ -0,0 +1,246 @@ +// Copyright 2020 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 +// +// StackThread.h -- a std::thread work-alike with a settable stack size. +// +// Platforms: Linux (glibc/musl), macOS, Windows (MSVC / clang-cl / MinGW-w64). +// Language: C++17. +// +// Differences from std::thread, all deliberate: +// * ctor takes a stack size in bytes as its first argument (0 = platform +// default) +// * optional thread name as a second argument, applied by the new thread +// itself +// (macOS only permits naming the calling thread, so this is the only +// portable point) +// * native_handle() is always available, not conditionally-supported +// * id is a distinct type from native_handle_type (they differ on Windows) +// +// Everything else -- move-only, terminate-on-joinable-destruction, INVOKE-style +// argument decay-copying, terminate on escaping exception -- matches +// std::thread. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace stellar +{ + +namespace detail +{ + +// Type-erased callable. Virtual dispatch rather than a function template keeps +// the trampoline a single non-template function, which avoids instantiating a +// C-callback-shaped function per callable type. +struct PayloadBase +{ + virtual ~PayloadBase() = default; + virtual void run() = 0; + std::string mName; +}; + +template struct Payload final : PayloadBase +{ + Fn mFn; + explicit Payload(Fn&& fn) : mFn(std::move(fn)) + { + } + void + run() override + { + mFn(); + } +}; + +void setCurrentThreadName(std::string const& name); +#if !defined(_WIN32) +std::size_t roundStackSize(std::size_t bytes); +bool isNullThread(::pthread_t const& t) noexcept; +#endif + +#if defined(_WIN32) +unsigned __stdcall trampoline(void* raw); +#else +void* trampoline(void* raw); +#endif + +} // namespace detail + +class StackThread +{ + public: +#if defined(_WIN32) + using native_handle_type = HANDLE; + using native_id_type = DWORD; +#else + using native_handle_type = ::pthread_t; + using native_id_type = ::pthread_t; +#endif + + // Opaque, comparable, hashable, streamable thread identity -- the analogue + // of std::thread::id. + class id + { + public: + id() noexcept; + + explicit id(native_id_type native) noexcept; + + friend bool operator==(id const& a, id const& b) noexcept; + + friend bool operator!=(id const& a, id const& b) noexcept; + + // Ordering exists so `id` can key a std::map. POSIX gives no ordering + // over pthread_t, so this compares the object representation. That is + // a strict weak ordering consistent with == on every implementation + // where pthread_t is a scalar (all of glibc, musl, macOS). + friend bool operator<(id const& a, id const& b) noexcept; + + friend bool operator>(id const& a, id const& b) noexcept; + friend bool operator<=(id const& a, id const& b) noexcept; + friend bool operator>=(id const& a, id const& b) noexcept; + + template + friend std::basic_ostream& + operator<<(std::basic_ostream& os, id const& v) + { + if (!v.mValid) + { + return os << "thread::id(of a non-executing thread)"; + } + std::uintptr_t scalar = 0; + std::memcpy(&scalar, &v.mNative, + std::min(sizeof(scalar), sizeof(native_id_type))); + return os << scalar; + } + + std::size_t hash() const noexcept; + + private: + native_id_type mNative; + bool mValid; + }; + + StackThread() noexcept = default; + + // Primary constructor. stackBytes == 0 selects the platform default; any + // other value is rounded up to satisfy platform minimums. + template , std::decay_t...>>> + StackThread(std::size_t stackBytes, F&& f, Args&&... args) + : StackThread(stackBytes, std::string{}, std::forward(f), + std::forward(args)...) + { + } + + // Same, but the new thread names itself before running the callable. + template , std::decay_t...>>> + StackThread(std::size_t stackBytes, std::string name, F&& f, Args&&... args) + { + // Decay-copy everything up front, exactly as std::thread does, so the + // new thread never touches the caller's storage. + auto bound = + [tup = std::make_tuple( + std::decay_t(std::forward(f)), + std::decay_t(std::forward(args))...)]() mutable { + std::apply( + [](auto&& fn, auto&&... rest) { + std::invoke(std::forward(fn), + std::forward(rest)...); + }, + std::move(tup)); + }; + + auto payload = std::make_unique>( + std::move(bound)); + payload->mName = std::move(name); + + start(stackBytes, std::move(payload)); + } + + StackThread(StackThread const&) = delete; + StackThread& operator=(StackThread const&) = delete; + + StackThread(StackThread&& other) noexcept; + + StackThread& operator=(StackThread&& other) noexcept; + + ~StackThread(); + + bool joinable() const noexcept; + + void join(); + + void detach(); + + id get_id() const noexcept; + + // Valid only while joinable(). On POSIX this is the pthread_t, suitable for + // pthread_setaffinity_np, pthread_setschedparam, pthread_getattr_np, etc. + // + // Caveat: joinable() only means "not yet joined or detached", not "still + // running". Once the thread function returns, the pthread_t stays valid as + // a join target but the underlying kernel task is gone, so scheduling and + // affinity calls will fail with ESRCH. If you intend to pin or reprioritise + // a thread, do it promptly after construction, or have the thread do it to + // itself. (Windows HANDLEs do not have this problem -- they stay queryable + // after exit.) + native_handle_type native_handle() const noexcept; + + void swap(StackThread& other) noexcept; + + static unsigned int hardware_concurrency() noexcept; + + // Convenience: name the calling thread. Note macOS can only name itself, + // so there is intentionally no name-another-thread entry point. + static void setCurrentName(std::string const& name); + + private: + void start(std::size_t stackBytes, + std::unique_ptr payload); + +#if defined(_WIN32) + HANDLE mHandle = nullptr; +#else + ::pthread_t mThread{}; +#endif +}; + +void swap(StackThread& a, StackThread& b) noexcept; + +} // namespace stellar + +namespace std +{ +template <> struct hash<::stellar::StackThread::id> +{ + std::size_t operator()(::stellar::StackThread::id const& v) const noexcept; +}; +} // namespace std diff --git a/src/util/test/StackThreadTests.cpp b/src/util/test/StackThreadTests.cpp new file mode 100644 index 0000000000..c2d7d8da5d --- /dev/null +++ b/src/util/test/StackThreadTests.cpp @@ -0,0 +1,233 @@ +// Copyright 2026 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "test/Catch2.h" +#include "util/BatchExecutor.h" +#include "util/StackThread.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#endif + +using stellar::StackThread; + +namespace +{ + +static void +freeFn(std::atomic* counter, int a, int b) +{ + *counter += a + b; +} + +struct Functor +{ + std::atomic* mCounter; + + void + operator()(std::string s) const + { + *mCounter += static_cast(s.size()); + } +}; + +struct MoveOnly +{ + std::unique_ptr p; +}; + +bool +waitUntil(std::function const& pred) +{ + auto const deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!pred()) + { + if (std::chrono::steady_clock::now() >= deadline) + { + return false; + } + std::this_thread::yield(); + } + return true; +} + +} + +TEST_CASE("StackThread invokes free function with arguments", "[stackthread]") +{ + std::atomic counter{0}; + StackThread t(1 << 20, freeFn, &counter, 3, 4); + REQUIRE(t.joinable()); + t.join(); + REQUIRE_FALSE(t.joinable()); + REQUIRE(counter == 7); +} + +#if defined(__linux__) +TEST_CASE("StackThread applies requested stack size and name", "[stackthread]") +{ + std::size_t observed = 0; + std::string name; + StackThread t( + stellar::WORKER_STACK_BYTES, "deep-worker", [&observed, &name] { + pthread_attr_t a; + if (pthread_getattr_np(pthread_self(), &a) == 0) + { + void* base = nullptr; + pthread_attr_getstack(&a, &base, &observed); + pthread_attr_destroy(&a); + } + char nameBuf[32] = {0}; + pthread_getname_np(pthread_self(), nameBuf, sizeof(nameBuf)); + name = nameBuf; + }); + t.join(); + REQUIRE(observed >= stellar::WORKER_STACK_BYTES); + REQUIRE(name == "deep-worker"); +} +#endif + +TEST_CASE("StackThread invokes functor with by-value string", "[stackthread]") +{ + std::atomic counter{0}; + StackThread t(0, Functor{&counter}, std::string("hello")); + t.join(); + REQUIRE(counter == 5); +} + +TEST_CASE("StackThread invokes callable with move-only argument", + "[stackthread]") +{ + std::atomic counter{0}; + MoveOnly m{std::make_unique(42)}; + StackThread t( + 1 << 16, [&counter](MoveOnly mo) { counter += *mo.p; }, std::move(m)); + t.join(); + REQUIRE(counter == 42); +} + +TEST_CASE("StackThread invokes member function pointer", "[stackthread]") +{ + std::atomic counter{0}; + struct S + { + std::atomic* mCounter; + int v = 5; + void + bump(int n) + { + *mCounter += v * n; + } + } s{&counter}; + StackThread t(1 << 16, &S::bump, &s, 2); + t.join(); + REQUIRE(counter == 10); +} + +TEST_CASE("StackThread supports move construction assignment and swap", + "[stackthread]") +{ + StackThread a(1 << 16, [] {}); + auto aid = a.get_id(); + StackThread b(std::move(a)); + REQUIRE_FALSE(a.joinable()); + REQUIRE(b.joinable()); + REQUIRE(b.get_id() == aid); + StackThread c; + c = std::move(b); + REQUIRE(c.joinable()); + REQUIRE_FALSE(b.joinable()); + swap(c, b); + REQUIRE(b.joinable()); + REQUIRE_FALSE(c.joinable()); + b.join(); +} + +TEST_CASE("StackThread id supports default construction lookup and streaming", + "[stackthread]") +{ + StackThread::id d1, d2; + REQUIRE(d1 == d2); + StackThread t(1 << 16, [] {}); + REQUIRE(t.get_id() != d1); + std::map m; + std::unordered_map um; + m[t.get_id()] = 1; + um[t.get_id()] = 1; + REQUIRE(m.count(t.get_id()) == 1); + REQUIRE(um.count(t.get_id()) == 1); + std::ostringstream os; + os << t.get_id(); + REQUIRE_FALSE(os.str().empty()); + t.join(); + REQUIRE(t.get_id() == d1); +} + +#if !defined(_WIN32) +TEST_CASE("StackThread native handle is usable while thread is running", + "[stackthread]") +{ + std::atomic go{false}, up{false}; + StackThread t(1 << 16, [&] { + up = true; + while (!go) + { + std::this_thread::yield(); + } + }); + REQUIRE(waitUntil([&] { return up.load(); })); + + int policy = 0; + sched_param sp{}; + int rc = pthread_getschedparam(t.native_handle(), &policy, &sp); + REQUIRE(rc == 0); + +#ifdef __linux__ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(0, &set); + rc = pthread_setaffinity_np(t.native_handle(), sizeof(set), &set); + CHECK(rc != ESRCH); +#endif + + go = true; + t.join(); +} +#endif + +TEST_CASE("StackThread supports detach", "[stackthread]") +{ + std::atomic done{false}; + StackThread t(1 << 16, [&done] { done = true; }); + t.detach(); + REQUIRE_FALSE(t.joinable()); + REQUIRE(waitUntil([&] { return done.load(); })); +} + +TEST_CASE("StackThread join on non-joinable throws", "[stackthread]") +{ + StackThread t; + REQUIRE_THROWS_AS(t.join(), std::system_error); +} + +TEST_CASE("StackThread clamps tiny stack requests to platform minimum", + "[stackthread]") +{ + StackThread t(1, [] {}); + t.join(); +} \ No newline at end of file