Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
66 changes: 64 additions & 2 deletions lightweightsemaphore.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@
#include <ctime> // For clock_gettime
#include <type_traits> // For std::make_signed<T>

#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(<semaphore>)
#include <semaphore>
#include <chrono>
#define MOODYCAMEL_HAS_STD_SEMAPHORE
#endif
#endif
#endif

#if defined(MOODYCAMEL_HAS_STD_SEMAPHORE)
// Using standard C++20 <semaphore>; 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).
Expand Down Expand Up @@ -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<MaxCount> 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<std::ptrdiff_t>(count));
}
}
};
#elif defined(_WIN32)
class Semaphore
{
private:
Expand Down
59 changes: 59 additions & 0 deletions tests/unittests/unittests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ class ConcurrentQueueTests : public TestClass<ConcurrentQueueTests>
// 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);
Expand Down Expand Up @@ -4672,6 +4673,64 @@ class ConcurrentQueueTests : public TestClass<ConcurrentQueueTests>

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<int> completed(0);
std::vector<SimpleThread> 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
{
Expand Down