diff --git a/doc/modules/ROOT/pages/execution/thread-pool.adoc b/doc/modules/ROOT/pages/execution/thread-pool.adoc index 0fb75944..41f0d2f0 100644 --- a/doc/modules/ROOT/pages/execution/thread-pool.adoc +++ b/doc/modules/ROOT/pages/execution/thread-pool.adoc @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -42,9 +43,12 @@ thread_pool pool2(4); // Single thread (useful for testing) thread_pool pool3(1); + +// Custom thread name prefix +thread_pool pool4(4, "myapp-io-"); // Threads named myapp-io-0, myapp-io-1, etc. ---- -The thread count cannot be changed after construction. +The thread count and thread name prefix cannot be changed after construction. == Getting an Executor @@ -246,6 +250,38 @@ thread_pool compute_pool; thread_pool io_pool(16); ---- +== Thread Naming + +Worker threads are automatically named for debugging purposes: + +* Threads are named `capy-pool-0`, `capy-pool-1`, etc. by default +* The prefix is configurable via the constructor's second parameter +* Names appear in debuggers (GDB, LLDB, Visual Studio) +* Names appear in system tools (`htop`, Process Explorer, `ps -L`) + +[source,cpp] +---- +// Default naming: capy-pool-0, capy-pool-1, ... +thread_pool pool1(4); + +// Custom naming: worker-0, worker-1, ... +thread_pool pool2(4, "worker-"); +---- + +This aids in identifying thread pool workers when debugging multi-threaded +applications with multiple pools. + +=== Name Length Limits + +The thread name prefix is truncated to 12 characters, leaving room for up +to 3-digit thread indices (0-999). + +For example, with prefix `"my-worker-"` and 100 threads, names would be +`my-worker-0` through `my-worker-99`. + +NOTE: Thread naming is a best-effort feature. On unsupported platforms, +names may not appear. + == Common Patterns === Single-Threaded Testing diff --git a/include/boost/capy/detail/thread_name.hpp b/include/boost/capy/detail/thread_name.hpp new file mode 100644 index 00000000..0310571e --- /dev/null +++ b/include/boost/capy/detail/thread_name.hpp @@ -0,0 +1,49 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_DETAIL_THREAD_NAME_HPP +#define BOOST_CAPY_DETAIL_THREAD_NAME_HPP + +#include + +/* + Thread naming abstraction for debugging purposes. + + Sets the current thread's name which appears in debuggers + (GDB, LLDB, Visual Studio) and system tools (htop, Process Explorer). + + Platform support: + - Windows: SetThreadDescription (Windows 10 1607+) + - macOS: pthread_setname_np (truncated to 63 chars) + - Linux/FreeBSD/NetBSD: pthread_setname_np (truncated to 15 chars) + - Other platforms: no-op +*/ + +namespace boost { +namespace capy { +namespace detail { + +/** Set the name of the current thread for debugging purposes. + + The name may be truncated to platform limits: + - Linux/FreeBSD/NetBSD: 15 characters + - macOS: 63 characters + - Windows: no practical limit + + @param name The thread name to set (UTF-8 encoded on Windows). +*/ +BOOST_CAPY_DECL +void +set_current_thread_name(char const* name) noexcept; + +} // detail +} // capy +} // boost + +#endif diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp index 23bc496b..9e0a61ef 100644 --- a/include/boost/capy/ex/thread_pool.hpp +++ b/include/boost/capy/ex/thread_pool.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -14,6 +15,7 @@ #include #include #include +#include namespace boost { namespace capy { @@ -62,9 +64,16 @@ class BOOST_CAPY_DECL @param num_threads The number of worker threads, or zero for automatic selection. + + @param thread_name_prefix The prefix for worker thread names. + Thread names appear as "{prefix}0", "{prefix}1", etc. + The prefix is truncated to 12 characters. Defaults to + "capy-pool-". */ explicit - thread_pool(std::size_t num_threads = 0); + thread_pool( + std::size_t num_threads = 0, + std::string_view thread_name_prefix = "capy-pool-"); thread_pool(thread_pool const&) = delete; thread_pool& operator=(thread_pool const&) = delete; diff --git a/src/detail/thread_name.cpp b/src/detail/thread_name.cpp new file mode 100644 index 00000000..5b62cb7f --- /dev/null +++ b/src/detail/thread_name.cpp @@ -0,0 +1,102 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#include + +#if defined(_WIN32) + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +#elif defined(__APPLE__) + +#include +#include + +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) + +#include +#include + +#endif + +/* + Platform-specific thread naming implementation. + + Each platform has a different API and name length limit: + - Windows: SetThreadDescription with UTF-8 to UTF-16 conversion (no limit) + - macOS: pthread_setname_np(name) with 63-char limit + - Linux/BSD: pthread_setname_np(thread, name) with 15-char limit + + All operations are best-effort and silently fail on error, since thread + naming is purely for debugging visibility and should never affect program + correctness. The noexcept guarantee is maintained by catching exceptions + from std::wstring allocation on Windows. +*/ + +namespace boost { +namespace capy { +namespace detail { + +void +set_current_thread_name(char const* name) noexcept +{ +#if defined(_WIN32) + // SetThreadDescription requires Windows 10 1607+. Older Windows versions + // are unsupported; the program may fail to link on those systems. + + // Query required buffer size for UTF-8 to wide conversion. + int required = MultiByteToWideChar(CP_UTF8, 0, name, -1, nullptr, 0); + if(required <= 0) + return; + + // Allocate and convert; catch exceptions to maintain noexcept. + std::wstring wname; + try + { + wname.resize(static_cast(required)); + } + catch(...) + { + return; + } + + if(MultiByteToWideChar(CP_UTF8, 0, name, -1, wname.data(), required) <= 0) + return; + + // Ignore return value: thread naming is best-effort for debugging. + (void)SetThreadDescription(GetCurrentThread(), wname.c_str()); +#elif defined(__APPLE__) + // macOS pthread_setname_np takes only the name (no thread handle) + // and has a 64 char limit (63 + null terminator) + char truncated[64]; + std::strncpy(truncated, name, 63); + truncated[63] = '\0'; + + // Ignore return value: thread naming is best-effort for debugging. + (void)pthread_setname_np(truncated); +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) + // pthread_setname_np has 16 char limit (15 + null terminator) + char truncated[16]; + std::strncpy(truncated, name, 15); + truncated[15] = '\0'; + + // Ignore return value: thread naming is best-effort for debugging. + (void)pthread_setname_np(pthread_self(), truncated); +#else + (void)name; +#endif +} + +} // detail +} // capy +} // boost diff --git a/src/ex/thread_pool.cpp b/src/ex/thread_pool.cpp index 4cccb67a..60eef636 100644 --- a/src/ex/thread_pool.cpp +++ b/src/ex/thread_pool.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -9,12 +10,32 @@ #include #include +#include #include +#include #include #include #include #include +/* + Thread pool implementation using a shared work queue. + + Work items are coroutine handles wrapped in intrusive list nodes, stored + in a single queue protected by a mutex. Worker threads wait on a + condition_variable_any that integrates with std::stop_token for clean + shutdown. + + Threads are started lazily on first post() via std::call_once to avoid + spawning threads for pools that are constructed but never used. Each + thread is named with a configurable prefix plus index for debugger + visibility. + + Shutdown sequence: stop() requests all threads to stop via their stop + tokens, then the destructor joins threads and destroys any remaining + queued work without executing it. +*/ + namespace boost { namespace capy { @@ -49,6 +70,7 @@ class thread_pool::impl detail::intrusive_queue q_; std::vector threads_; std::size_t num_threads_; + char thread_name_prefix_[13]{}; // 12 chars max + null terminator std::once_flag start_flag_; public: @@ -61,14 +83,17 @@ class thread_pool::impl w->destroy(); } - explicit - impl(std::size_t num_threads) + impl(std::size_t num_threads, std::string_view thread_name_prefix) : num_threads_(num_threads) { if(num_threads_ == 0) num_threads_ = std::thread::hardware_concurrency(); if(num_threads_ == 0) num_threads_ = 1; + + // Truncate prefix to 12 chars, leaving room for up to 3-digit index. + auto n = thread_name_prefix.copy(thread_name_prefix_, 12); + thread_name_prefix_[n] = '\0'; } void @@ -98,13 +123,19 @@ class thread_pool::impl std::call_once(start_flag_, [this]{ threads_.reserve(num_threads_); for(std::size_t i = 0; i < num_threads_; ++i) - threads_.emplace_back([this](std::stop_token st){ run(st); }); + threads_.emplace_back( + [this, i](std::stop_token st){ run(st, i); }); }); } void - run(std::stop_token st) + run(std::stop_token st, std::size_t index) { + // Build name; set_current_thread_name truncates to platform limits. + char name[16]; + std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index); + detail::set_current_thread_name(name); + for(;;) { work* w = nullptr; @@ -130,8 +161,8 @@ thread_pool:: } thread_pool:: -thread_pool(std::size_t num_threads) - : impl_(new impl(num_threads)) +thread_pool(std::size_t num_threads, std::string_view thread_name_prefix) + : impl_(new impl(num_threads, thread_name_prefix)) { } diff --git a/test/unit/ex/thread_pool.cpp b/test/unit/ex/thread_pool.cpp index d52ff801..c661cd82 100644 --- a/test/unit/ex/thread_pool.cpp +++ b/test/unit/ex/thread_pool.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -12,11 +13,9 @@ #include -#include "test_suite.hpp" +#include "test_helpers.hpp" #include -#include -#include #include namespace boost { @@ -43,19 +42,53 @@ struct test_service : execution_context::service void shutdown() override {} }; -// Helper to wait for a condition with timeout -template -bool wait_for(Pred pred, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)) +#if defined(BOOST_CAPY_TEST_CAN_GET_THREAD_NAME) +// Result storage for thread name check +struct name_check_result { - auto start = std::chrono::steady_clock::now(); - while(!pred()) + std::atomic done{false}; + std::atomic matches{false}; +}; + +// Coroutine that checks thread name when resumed on pool thread. +// Arguments are forwarded to promise_type constructor. +struct name_checker +{ + struct promise_type { - if(std::chrono::steady_clock::now() - start > timeout) - return false; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - return true; + name_check_result& result; + char const* prefix; + + promise_type(name_check_result& r, char const* p) + : result(r), prefix(p) {} + + name_checker get_return_object() + { + return {std::coroutine_handle::from_promise(*this)}; + } + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_never final_suspend() noexcept + { + result.matches.store(thread_name_starts_with(prefix)); + result.done.store(true); + return {}; + } + void return_void() {} + void unhandled_exception() {} + }; + + std::coroutine_handle h; + operator coro() const { return h; } +}; + +name_checker check_thread_name(name_check_result& result, char const* prefix) +{ + // Parameters forwarded to promise_type constructor, not used here. + (void)result; + (void)prefix; + co_return; } +#endif } // namespace @@ -234,6 +267,54 @@ struct thread_pool_test (void)ex; } + void + testThreadNaming() + { + // Test custom naming prefix (construction only) + { + thread_pool pool(2, "test-worker-"); + (void)pool.get_executor(); + } + + // Test empty prefix + { + thread_pool pool(1, ""); + (void)pool.get_executor(); + } + +#if defined(BOOST_CAPY_TEST_CAN_GET_THREAD_NAME) + // Verify default thread name from within pool thread + { + thread_pool pool(1); + name_check_result result; + pool.get_executor().post(check_thread_name(result, "capy-pool-")); + + BOOST_TEST(wait_for([&]{ return result.done.load(); })); + BOOST_TEST(result.matches.load()); + } + + // Verify custom thread name from within pool thread + { + thread_pool pool(1, "mypool-"); + name_check_result result; + pool.get_executor().post(check_thread_name(result, "mypool-")); + + BOOST_TEST(wait_for([&]{ return result.done.load(); })); + BOOST_TEST(result.matches.load()); + } + + // Verify thread naming works with index suffix + { + thread_pool pool(1, "idx-"); + name_check_result result; + pool.get_executor().post(check_thread_name(result, "idx-0")); + + BOOST_TEST(wait_for([&]{ return result.done.load(); })); + BOOST_TEST(result.matches.load()); + } +#endif + } + void run() { @@ -248,6 +329,7 @@ struct thread_pool_test testMakeService(); testConcurrentPost(); testDefaultExecutor(); + testThreadNaming(); } }; diff --git a/test/unit/test_helpers.hpp b/test/unit/test_helpers.hpp index 5320587e..d956c901 100644 --- a/test/unit/test_helpers.hpp +++ b/test/unit/test_helpers.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2021 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -22,6 +23,21 @@ #include "test_suite.hpp" +#include +#include +#include + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__) +#include +#define BOOST_CAPY_TEST_CAN_GET_THREAD_NAME 1 +#elif defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#define BOOST_CAPY_TEST_CAN_GET_THREAD_NAME 1 +#endif + namespace boost { namespace capy { @@ -108,6 +124,85 @@ struct test_executor static_assert(Executor); +//---------------------------------------------------------- +// Wait Utilities +//---------------------------------------------------------- + +/// Wait for a predicate to become true with timeout. +template +bool +wait_for( + Pred pred, + std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)) +{ + auto start = std::chrono::steady_clock::now(); + while(!pred()) + { + if(std::chrono::steady_clock::now() - start > timeout) + return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} + +//---------------------------------------------------------- +// Thread Name Utilities +//---------------------------------------------------------- + +#if defined(BOOST_CAPY_TEST_CAN_GET_THREAD_NAME) + +/// Get current thread name into buffer, returns true on success. +inline bool +get_current_thread_name(char* buffer, std::size_t size) +{ +#if defined(_WIN32) + wchar_t* wname = nullptr; + if(!SUCCEEDED(GetThreadDescription(GetCurrentThread(), &wname))) + return false; + int len = WideCharToMultiByte( + CP_UTF8, 0, wname, -1, buffer, + static_cast(size), nullptr, nullptr); + LocalFree(wname); + return len > 0; +#else + return pthread_getname_np(pthread_self(), buffer, size) == 0; +#endif +} + +/// Check if current thread name matches expected value exactly. +inline bool +check_thread_name(char const* expected) +{ + char buffer[64] = {}; +#if defined(_WIN32) + wchar_t* wname = nullptr; + if(!SUCCEEDED(GetThreadDescription(GetCurrentThread(), &wname))) + return false; + int len = WideCharToMultiByte( + CP_UTF8, 0, wname, -1, buffer, + static_cast(sizeof(buffer)), nullptr, nullptr); + LocalFree(wname); + if(len <= 0) + return false; +#else + if(pthread_getname_np(pthread_self(), buffer, sizeof(buffer)) != 0) + return false; +#endif + return std::strcmp(buffer, expected) == 0; +} + +/// Check if current thread name starts with given prefix. +inline bool +thread_name_starts_with(char const* prefix) +{ + char buffer[64] = {}; + if(!get_current_thread_name(buffer, sizeof(buffer))) + return false; + return std::strncmp(buffer, prefix, std::strlen(prefix)) == 0; +} + +#endif // BOOST_CAPY_TEST_CAN_GET_THREAD_NAME + } // capy } // boost diff --git a/test/unit/thread_name.cpp b/test/unit/thread_name.cpp new file mode 100644 index 00000000..14836f1b --- /dev/null +++ b/test/unit/thread_name.cpp @@ -0,0 +1,78 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include "test_helpers.hpp" + +namespace boost { +namespace capy { + +struct thread_name_test +{ + void + testSetName() + { +#if defined(BOOST_CAPY_TEST_CAN_GET_THREAD_NAME) + detail::set_current_thread_name("test-thread"); + BOOST_TEST(check_thread_name("test-thread")); + + detail::set_current_thread_name("capy-pool-0"); + BOOST_TEST(check_thread_name("capy-pool-0")); + + // Long name is truncated to 15 chars on Linux/FreeBSD/NetBSD + detail::set_current_thread_name( + "this-is-a-very-long-thread-name-that-exceeds-limits"); +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) + BOOST_TEST(check_thread_name("this-is-a-very-")); +#elif defined(__APPLE__) + // macOS truncates to 63 chars + BOOST_TEST(check_thread_name( + "this-is-a-very-long-thread-name-that-exceeds-limits")); +#elif defined(_WIN32) + // Windows has no practical limit + BOOST_TEST(check_thread_name( + "this-is-a-very-long-thread-name-that-exceeds-limits")); +#endif + + // Test macOS 63-char limit specifically + detail::set_current_thread_name( + "0123456789012345678901234567890123456789012345678901234567890123456789"); +#if defined(__APPLE__) + // Truncated to 63 chars + BOOST_TEST(check_thread_name( + "012345678901234567890123456789012345678901234567890123456789012")); +#endif + +#if defined(_WIN32) + // Windows UTF-8 support (simple ASCII subset) + detail::set_current_thread_name("worker-thread-1"); + BOOST_TEST(check_thread_name("worker-thread-1")); +#endif +#endif // BOOST_CAPY_TEST_CAN_GET_THREAD_NAME + + // Empty string should not crash (but we don't verify the result + // since some platforms may not support clearing thread names) + detail::set_current_thread_name(""); + } + + void + run() + { + testSetName(); + } +}; + +TEST_SUITE( + thread_name_test, + "boost.capy.thread_name"); + +} // capy +} // boost