From e64ace14f2eef5df850b5b23bdfd783343bb812f Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 30 Jul 2026 13:55:08 -0700 Subject: [PATCH 1/6] Add StackThread --- src/util/StackThread.h | 586 +++++++++++++++++++++++++++++ src/util/test/StackThreadTests.cpp | 231 ++++++++++++ 2 files changed, 817 insertions(+) create mode 100644 src/util/StackThread.h create mode 100644 src/util/test/StackThreadTests.cpp diff --git a/src/util/StackThread.h b/src/util/StackThread.h new file mode 100644 index 0000000000..d07771af34 --- /dev/null +++ b/src/util/StackThread.h @@ -0,0 +1,586 @@ +// 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 an 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 +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +// 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 +#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(); + } +}; + +inline 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__) + // Self-only, and silently truncates. + ::pthread_setname_np(name.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. +inline 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; +} +#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) +inline unsigned __stdcall trampoline(void* raw) +#else +inline 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 + +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 : mNative{}, mValid(false) + { + } + + explicit id(native_id_type native) noexcept + : mNative(native), mValid(true) + { + } + + friend bool + operator==(id const& a, 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 + } + + friend bool + operator!=(id const& a, id const& b) noexcept + { + return !(a == b); + } + + // 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 + { + if (a.mValid != b.mValid) + { + return !a.mValid; + } + if (!a.mValid) + { + return false; + } + return std::memcmp(&a.mNative, &b.mNative, sizeof(native_id_type)) < + 0; + } + + friend bool + operator>(id const& a, id const& b) noexcept + { + return b < a; + } + friend bool + operator<=(id const& a, id const& b) noexcept + { + return !(b < a); + } + friend bool + operator>=(id const& a, id const& b) noexcept + { + return !(a < b); + } + + 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 + { + 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); + } + + 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 + { + swap(other); + } + + 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() + { + if (joinable()) + { + std::terminate(); + } + } + + bool + joinable() const noexcept + { +#if defined(_WIN32) + return mHandle != nullptr; +#else + return mJoinable; +#endif + } + + void + 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) + 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"); + } + mJoinable = false; +#endif + mId = id{}; + } + + void + 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"); + } + mJoinable = false; +#endif + mId = id{}; + } + + id + get_id() const noexcept + { + return mId; + } + + // 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 + { +#if defined(_WIN32) + return mHandle; +#else + return mThread; +#endif + } + + void + swap(StackThread& other) noexcept + { +#if defined(_WIN32) + std::swap(mHandle, other.mHandle); +#else + std::swap(mThread, other.mThread); + std::swap(mJoinable, other.mJoinable); +#endif + std::swap(mId, other.mId); + } + + static unsigned int + hardware_concurrency() noexcept + { + return std::thread::hardware_concurrency(); + } + + // 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) + { + detail::setCurrentThreadName(name); + } + + private: + void + 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. + unsigned threadId = 0; + uintptr_t const h = ::_beginthreadex( + nullptr, static_cast(stackBytes), &detail::trampoline, + payload.get(), STACK_SIZE_PARAM_IS_A_RESERVATION, &threadId); + if (h == 0) + { + throw std::system_error(errno, std::generic_category(), + "_beginthreadex"); + } + payload.release(); // ownership transferred to the new thread + mHandle = reinterpret_cast(h); + mId = id{static_cast(threadId)}; +#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"); + } + } + + ::pthread_t tid{}; + rc = ::pthread_create(&tid, &attr, &detail::trampoline, payload.get()); + if (rc != 0) + { + throw std::system_error(rc, std::system_category(), + "pthread_create"); + } + payload.release(); // ownership transferred to the new thread + mThread = tid; + mJoinable = true; + mId = id{tid}; +#endif + } + +#if defined(_WIN32) + HANDLE mHandle = nullptr; +#else + ::pthread_t mThread{}; + bool mJoinable = false; +#endif + id mId{}; +}; + +inline void +swap(StackThread& a, StackThread& b) noexcept +{ + a.swap(b); +} + +} // namespace stellar + +namespace std +{ +template <> struct hash<::stellar::StackThread::id> +{ + std::size_t + operator()(::stellar::StackThread::id const& v) const noexcept + { + return v.hash(); + } +}; +} // namespace std \ No newline at end of file diff --git a/src/util/test/StackThreadTests.cpp b/src/util/test/StackThreadTests.cpp new file mode 100644 index 0000000000..06da7b13dc --- /dev/null +++ b/src/util/test/StackThreadTests.cpp @@ -0,0 +1,231 @@ +// 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/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(4 * 1024 * 1024, "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 == 4u * 1024 * 1024); + 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 From 519b33abe039966e92f58417f25885f84c709735 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 30 Jul 2026 14:13:32 -0700 Subject: [PATCH 2/6] Switch BatchExecutor to StackThread with 8MiB stacks. --- src/util/BatchExecutor.cpp | 4 +++- src/util/BatchExecutor.h | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/util/BatchExecutor.cpp b/src/util/BatchExecutor.cpp index 324efc429b..6367f7f3a0 100644 --- a/src/util/BatchExecutor.cpp +++ b/src/util/BatchExecutor.cpp @@ -19,6 +19,8 @@ namespace stellar { +const size_t WORKER_STACK_BYTES = 1 << 23; // 8 MiB + namespace { struct CpuPinning @@ -160,7 +162,7 @@ BatchExecutor::ensureWorkers(size_t count) { size_t index = mWorkers.size(); uint64_t batchId = mBatchId; - mWorkers.emplace_back( + 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..a0a868a6a7 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,7 +16,6 @@ #include #include #include -#include #include #include @@ -77,7 +77,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. From 97b128093f9e18c5d5cb9bbdf946c4f888918172 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 30 Jul 2026 17:08:14 -0700 Subject: [PATCH 3/6] Tweak test to allow >= requested stack (needed on linux) --- src/util/test/StackThreadTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/test/StackThreadTests.cpp b/src/util/test/StackThreadTests.cpp index 06da7b13dc..d068bc1380 100644 --- a/src/util/test/StackThreadTests.cpp +++ b/src/util/test/StackThreadTests.cpp @@ -95,7 +95,7 @@ TEST_CASE("StackThread applies requested stack size and name", "[stackthread]") name = nameBuf; }); t.join(); - REQUIRE(observed == 4u * 1024 * 1024); + REQUIRE(observed >= 4u * 1024 * 1024); REQUIRE(name == "deep-worker"); } #endif From 48cd56e28d9699a38ac9de6200b435bbc733e9ca Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Thu, 30 Jul 2026 20:42:38 -0400 Subject: [PATCH 4/6] Windows updates --- Builds/VisualStudio/stellar-core.vcxproj | 2 ++ Builds/VisualStudio/stellar-core.vcxproj.filters | 6 ++++++ src/transactions/InvokeHostFunctionOpFrame.cpp | 6 ++++++ src/transactions/test/InvokeHostFunctionTests.cpp | 4 ---- src/util/StackThread.h | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index a68c0845ab..a6cee0b4bf 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 18cfe946f2..782dc75eb1 100644 --- a/src/transactions/test/InvokeHostFunctionTests.cpp +++ b/src/transactions/test/InvokeHostFunctionTests.cpp @@ -6862,9 +6862,6 @@ TEST_CASE("Soroban delegated signer authentication", "[soroban]") InvokeHostFunctionResultCode::INVOKE_HOST_FUNCTION_TRAPPED); } } - // This test causes stack overflow on Windows and macOS, but works fine on - // Linux. -#if !defined(WIN32) && !defined(__APPLE__) SECTION("deep delegate tree") { auto buildDelegateChain = [&](int depth) { @@ -6911,7 +6908,6 @@ TEST_CASE("Soroban delegated signer authentication", "[soroban]") InvokeHostFunctionResultCode::INVOKE_HOST_FUNCTION_TRAPPED); } } -#endif } TEST_CASE("Soroban authorization", "[tx][soroban]") diff --git a/src/util/StackThread.h b/src/util/StackThread.h index d07771af34..6122da1bce 100644 --- a/src/util/StackThread.h +++ b/src/util/StackThread.h @@ -583,4 +583,4 @@ template <> struct hash<::stellar::StackThread::id> return v.hash(); } }; -} // namespace std \ No newline at end of file +} // namespace std From 08fa0f5cb055a8d88c461353777b8c8acb05a894 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 31 Jul 2026 11:28:54 -0700 Subject: [PATCH 5/6] Fix some review comments --- src/util/StackThread.h | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/util/StackThread.h b/src/util/StackThread.h index 6122da1bce..3fc3783658 100644 --- a/src/util/StackThread.h +++ b/src/util/StackThread.h @@ -10,7 +10,7 @@ // Differences from std::thread, all deliberate: // * ctor takes a stack size in bytes as its first argument (0 = platform // default) -// * optional thread name as an second argument, applied by the new thread +// * 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) @@ -107,8 +107,9 @@ setCurrentThreadName(std::string const& name) setDesc(::GetCurrentThread(), wide.c_str()); } #elif defined(__APPLE__) - // Self-only, and silently truncates. - ::pthread_setname_np(name.c_str()); + // 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); @@ -393,6 +394,12 @@ class StackThread "StackThread::join on a non-joinable thread"); } #if defined(_WIN32) + if (mId == ::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) { From 4bb087f25c451133b1d20f0105900d914fd7fcdb Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 31 Jul 2026 15:31:59 -0700 Subject: [PATCH 6/6] Fix startup races, move to separate .cpp --- src/util/BatchExecutor.cpp | 7 +- src/util/BatchExecutor.h | 1 + src/util/StackThread.cpp | 447 +++++++++++++++++++++++++++++ src/util/StackThread.h | 407 ++------------------------ src/util/test/StackThreadTests.cpp | 28 +- 5 files changed, 496 insertions(+), 394 deletions(-) create mode 100644 src/util/StackThread.cpp diff --git a/src/util/BatchExecutor.cpp b/src/util/BatchExecutor.cpp index 6367f7f3a0..91a18e1eb1 100644 --- a/src/util/BatchExecutor.cpp +++ b/src/util/BatchExecutor.cpp @@ -19,8 +19,6 @@ namespace stellar { -const size_t WORKER_STACK_BYTES = 1 << 23; // 8 MiB - namespace { struct CpuPinning @@ -162,8 +160,9 @@ BatchExecutor::ensureWorkers(size_t count) { size_t index = mWorkers.size(); uint64_t batchId = mBatchId; - mWorkers.emplace_back(WORKER_STACK_BYTES, - [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 a0a868a6a7..ce13427765 100644 --- a/src/util/BatchExecutor.h +++ b/src/util/BatchExecutor.h @@ -21,6 +21,7 @@ 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. // 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 index 3fc3783658..2764dad4de 100644 --- a/src/util/StackThread.h +++ b/src/util/StackThread.h @@ -25,14 +25,12 @@ #include #include +#include #include -#include #include #include #include #include -#include -#include #include #include #include @@ -45,16 +43,8 @@ #define NOMINMAX #endif #include -// 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 -#include #endif namespace stellar @@ -86,103 +76,17 @@ template struct Payload final : PayloadBase } }; -inline 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 -} - +void setCurrentThreadName(std::string const& name); #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. -inline 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; -} +std::size_t roundStackSize(std::size_t bytes); +bool isNullThread(::pthread_t const& t) noexcept; #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) -inline unsigned __stdcall trampoline(void* raw) +unsigned __stdcall trampoline(void* raw); #else -inline void* -trampoline(void* raw) +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 @@ -202,73 +106,23 @@ class StackThread class id { public: - id() noexcept : mNative{}, mValid(false) - { - } + id() noexcept; - explicit id(native_id_type native) noexcept - : mNative(native), mValid(true) - { - } + explicit id(native_id_type native) noexcept; - friend bool - operator==(id const& a, 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 - } + friend bool operator==(id const& a, id const& b) noexcept; - friend bool - operator!=(id const& a, id const& b) noexcept - { - return !(a == b); - } + 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 - { - if (a.mValid != b.mValid) - { - return !a.mValid; - } - if (!a.mValid) - { - return false; - } - return std::memcmp(&a.mNative, &b.mNative, sizeof(native_id_type)) < - 0; - } + friend bool operator<(id const& a, id const& b) noexcept; - friend bool - operator>(id const& a, id const& b) noexcept - { - return b < a; - } - friend bool - operator<=(id const& a, id const& b) noexcept - { - return !(b < a); - } - friend bool - operator>=(id const& a, id const& b) noexcept - { - return !(a < b); - } + 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& @@ -284,18 +138,7 @@ class StackThread return os << scalar; } - std::size_t - 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); - } + std::size_t hash() const noexcept; private: native_id_type mNative; @@ -345,110 +188,19 @@ class StackThread StackThread(StackThread const&) = delete; StackThread& operator=(StackThread const&) = delete; - StackThread(StackThread&& other) noexcept - { - swap(other); - } + StackThread(StackThread&& other) noexcept; - 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& operator=(StackThread&& other) noexcept; - ~StackThread() - { - if (joinable()) - { - std::terminate(); - } - } + ~StackThread(); - bool - joinable() const noexcept - { -#if defined(_WIN32) - return mHandle != nullptr; -#else - return mJoinable; -#endif - } + bool joinable() const noexcept; - void - 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 (mId == ::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"); - } - mJoinable = false; -#endif - mId = id{}; - } + void join(); - void - 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"); - } - mJoinable = false; -#endif - mId = id{}; - } + void detach(); - id - get_id() const noexcept - { - return mId; - } + 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. @@ -460,123 +212,28 @@ class StackThread // 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 - { -#if defined(_WIN32) - return mHandle; -#else - return mThread; -#endif - } + native_handle_type native_handle() const noexcept; - void - swap(StackThread& other) noexcept - { -#if defined(_WIN32) - std::swap(mHandle, other.mHandle); -#else - std::swap(mThread, other.mThread); - std::swap(mJoinable, other.mJoinable); -#endif - std::swap(mId, other.mId); - } + void swap(StackThread& other) noexcept; - static unsigned int - hardware_concurrency() noexcept - { - return std::thread::hardware_concurrency(); - } + 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) - { - detail::setCurrentThreadName(name); - } + static void setCurrentName(std::string const& name); private: - void - 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. - unsigned threadId = 0; - uintptr_t const h = ::_beginthreadex( - nullptr, static_cast(stackBytes), &detail::trampoline, - payload.get(), STACK_SIZE_PARAM_IS_A_RESERVATION, &threadId); - if (h == 0) - { - throw std::system_error(errno, std::generic_category(), - "_beginthreadex"); - } - payload.release(); // ownership transferred to the new thread - mHandle = reinterpret_cast(h); - mId = id{static_cast(threadId)}; -#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"); - } - } - - ::pthread_t tid{}; - rc = ::pthread_create(&tid, &attr, &detail::trampoline, payload.get()); - if (rc != 0) - { - throw std::system_error(rc, std::system_category(), - "pthread_create"); - } - payload.release(); // ownership transferred to the new thread - mThread = tid; - mJoinable = true; - mId = id{tid}; -#endif - } + void start(std::size_t stackBytes, + std::unique_ptr payload); #if defined(_WIN32) HANDLE mHandle = nullptr; #else ::pthread_t mThread{}; - bool mJoinable = false; #endif - id mId{}; }; -inline void -swap(StackThread& a, StackThread& b) noexcept -{ - a.swap(b); -} +void swap(StackThread& a, StackThread& b) noexcept; } // namespace stellar @@ -584,10 +241,6 @@ namespace std { template <> struct hash<::stellar::StackThread::id> { - std::size_t - operator()(::stellar::StackThread::id const& v) const noexcept - { - return v.hash(); - } + 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 index d068bc1380..c2d7d8da5d 100644 --- a/src/util/test/StackThreadTests.cpp +++ b/src/util/test/StackThreadTests.cpp @@ -3,6 +3,7 @@ // 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 @@ -82,20 +83,21 @@ TEST_CASE("StackThread applies requested stack size and name", "[stackthread]") { std::size_t observed = 0; std::string name; - StackThread t(4 * 1024 * 1024, "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; - }); + 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 >= 4u * 1024 * 1024); + REQUIRE(observed >= stellar::WORKER_STACK_BYTES); REQUIRE(name == "deep-worker"); } #endif