From 5b4a545959bddf3cb32a85aebf3c46757fdb4031 Mon Sep 17 00:00:00 2001 From: AnmolS05 Date: Mon, 5 Oct 2026 23:09:29 +0530 Subject: [PATCH] Add portable C++20 std::counting_semaphore support to LightweightSemaphore (#449) Add a standard C++20 std::counting_semaphore implementation of details::Semaphore in lightweightsemaphore.h. This provides cross-platform portability without requiring OS-specific synchronization primitives (CreateSemaphoreW, mach, sem_t) and speeds up blocking queue construction on Windows. Supports opt-out via MOODYCAMEL_USE_OS_SEMAPHORE and opt-in via MOODYCAMEL_USE_STD_SEMAPHORE. Adds comprehensive unit test coverage in tests/unittests/unittests.cpp. --- CHANGELOG.md | 12 +++++++ lightweightsemaphore.h | 66 +++++++++++++++++++++++++++++++++-- tests/unittests/unittests.cpp | 59 +++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..204b1793 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## [Unreleased] - 2026-10-05T23:09:00+05:30 + +### Added +- Added standard C++20 `std::counting_semaphore` implementation of `moodycamel::details::Semaphore` in `lightweightsemaphore.h`, enabling portable cross-platform semaphore synchronization without requiring OS-specific APIs (`CreateSemaphoreW`, `mach/mach.h`, or `semaphore.h`) (Issue #449). +- Added opt-out / opt-in configuration macros `MOODYCAMEL_USE_OS_SEMAPHORE` and `MOODYCAMEL_USE_STD_SEMAPHORE`. +- Added unit test `details_semaphore_basic_and_threaded` in `tests/unittests/unittests.cpp` validating non-blocking try-wait, timed wait, single and multi-signal, and concurrent thread signaling. + +### Files Affected +- `lightweightsemaphore.h` +- `tests/unittests/unittests.cpp` diff --git a/lightweightsemaphore.h b/lightweightsemaphore.h index c103068f..c30a51b0 100644 --- a/lightweightsemaphore.h +++ b/lightweightsemaphore.h @@ -13,7 +13,19 @@ #include // For clock_gettime #include // For std::make_signed -#if defined(_WIN32) +#if !defined(MOODYCAMEL_USE_OS_SEMAPHORE) && (defined(MOODYCAMEL_USE_STD_SEMAPHORE) || (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) +#if defined(__has_include) +#if __has_include() +#include +#include +#define MOODYCAMEL_HAS_STD_SEMAPHORE +#endif +#endif +#endif + +#if defined(MOODYCAMEL_HAS_STD_SEMAPHORE) +// Using standard C++20 ; OS-specific includes and declarations are not required +#elif defined(_WIN32) // Avoid including windows.h in a header; we only need a handful of // items, so we'll redeclare them here (this is relatively safe since // the API generally has to remain stable between Windows versions). @@ -74,7 +86,57 @@ namespace details // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. -#if defined(_WIN32) +#if defined(MOODYCAMEL_HAS_STD_SEMAPHORE) +//--------------------------------------------------------- +// Semaphore (C++20 std::counting_semaphore) +//--------------------------------------------------------- +class Semaphore +{ +private: + static const std::ptrdiff_t MaxCount = 0x7fffffff; + std::counting_semaphore m_sema; + + Semaphore(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; + Semaphore& operator=(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; + +public: + Semaphore(int initialCount = 0) + : m_sema(initialCount) + { + assert(initialCount >= 0); + } + + bool wait() + { + m_sema.acquire(); + return true; + } + + bool try_wait() + { + return m_sema.try_acquire(); + } + + bool timed_wait(std::uint64_t usecs) + { + return m_sema.try_acquire_for(std::chrono::microseconds(usecs)); + } + + void signal() + { + m_sema.release(1); + } + + void signal(int count) + { + assert(count >= 0); + if (count > 0) + { + m_sema.release(static_cast(count)); + } + } +}; +#elif defined(_WIN32) class Semaphore { private: diff --git a/tests/unittests/unittests.cpp b/tests/unittests/unittests.cpp index 7371f03e..428cacfc 100644 --- a/tests/unittests/unittests.cpp +++ b/tests/unittests/unittests.cpp @@ -378,6 +378,7 @@ class ConcurrentQueueTests : public TestClass // Semaphore REGISTER_TEST(acquire_and_signal); REGISTER_TEST(try_acquire_and_signal); + REGISTER_TEST(details_semaphore_basic_and_threaded); // Core algos REGISTER_TEST(core_add_only_list); @@ -4672,6 +4673,64 @@ class ConcurrentQueueTests : public TestClass return true; } + + /** + * @brief Validates details::Semaphore basic operations, multi-signal, timed wait, + * and concurrent acquire/release behavior across threads. + * + * @return true if all assertions succeed. + */ + bool details_semaphore_basic_and_threaded() + { + details::Semaphore sem(0); + + // Non-blocking try_wait on 0 count + ASSERT_OR_FAIL(!sem.try_wait()); + + // Timed wait on 0 count should time out + ASSERT_OR_FAIL(!sem.timed_wait(1000)); // 1ms + + // Signal single + sem.signal(); + ASSERT_OR_FAIL(sem.try_wait()); + ASSERT_OR_FAIL(!sem.try_wait()); + + // Signal multiple + sem.signal(5); + for (int i = 0; i < 5; ++i) { + ASSERT_OR_FAIL(sem.wait()); + } + ASSERT_OR_FAIL(!sem.try_wait()); + + // Concurrent wait and signal + const int THREAD_COUNT = 4; + const int OPS_PER_THREAD = 10; + std::atomic completed(0); + std::vector threads(THREAD_COUNT); + + for (int i = 0; i < THREAD_COUNT; ++i) { + threads[i] = SimpleThread([&]() { + for (int j = 0; j < OPS_PER_THREAD; ++j) { + if (sem.wait()) { + completed.fetch_add(1, std::memory_order_relaxed); + } + } + }); + } + + for (int j = 0; j < OPS_PER_THREAD; ++j) { + sem.signal(THREAD_COUNT); + } + + for (int i = 0; i < THREAD_COUNT; ++i) { + threads[i].join(); + } + + ASSERT_OR_FAIL(completed.load() == THREAD_COUNT * OPS_PER_THREAD); + ASSERT_OR_FAIL(!sem.try_wait()); + + return true; + } struct TestListItem : corealgos::ListItem {