Skip to content

Commit 0bc424c

Browse files
committed
Drop zero-timeout pthread_mutex_trylock() optimization
Previously, if the mutex protecting the inner state of the event object were locked by another thread, the early abort in `WaitForEvent()` with a zero-ms timeout would have translated that to a `WAIT_TIMEOUT` result, even if the application logic is such that the event in question is always set (e.g. simultaneous calls to `WaitForEvent()` and `SetEvent()` on an already-set event). This patch makes `event->State` an atomic that can be used as a synchronization point (currently in addition to rather than instead of `event->Mutex`). Note: Unlocking the spinlock (i.e. resetting the event) is done with release semantics rather than using `std::memory_order_relaxed` due to ambiguities in the C++ spec that make it unclear whether or not consecutive Set/Reset blocks could be interleaved with the latter; see the discussion preceding [0] for reference. Closes #18. [0]: https://old.reddit.com/r/cpp/comments/g84bzv/c/fpua2yq/
1 parent 16967b9 commit 0bc424c

File tree

1 file changed

+34
-24
lines changed

1 file changed

+34
-24
lines changed

src/pevents.cpp

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include <errno.h>
1313
#include <pthread.h>
1414
#include <sys/time.h>
15+
#include <atomic>
16+
#include <memory>
1517
#ifdef WFMO
1618
#include <algorithm>
1719
#include <deque>
@@ -54,7 +56,7 @@ namespace neosmart {
5456
pthread_cond_t CVariable;
5557
pthread_mutex_t Mutex;
5658
bool AutoReset;
57-
bool State;
59+
std::atomic<bool> State;
5860
#ifdef WFMO
5961
std::deque<neosmart_wfmo_info_t_> RegisteredWaits;
6062
#endif
@@ -100,7 +102,8 @@ namespace neosmart {
100102
result = pthread_mutex_init(&event->Mutex, 0);
101103
assert(result == 0);
102104

103-
event->State = false;
105+
// memory_order_relaxed: Newly created event is guaranteed to not have any waiters
106+
event->State.store(false, std::memory_order_relaxed);
104107
event->AutoReset = !manualReset;
105108

106109
if (initialState) {
@@ -113,7 +116,8 @@ namespace neosmart {
113116

114117
static int UnlockedWaitForEvent(neosmart_event_t event, uint64_t milliseconds) {
115118
int result = 0;
116-
if (!event->State) {
119+
// memory_order_relaxed: unlocking/ordering is guaranteed prior to calling this function
120+
if (!event->State.load(std::memory_order_relaxed)) {
117121
// Zero-timeout event state check optimization
118122
if (milliseconds == 0) {
119123
return WAIT_TIMEOUT;
@@ -139,17 +143,20 @@ namespace neosmart {
139143
} else {
140144
result = pthread_cond_wait(&event->CVariable, &event->Mutex);
141145
}
142-
} while (result == 0 && !event->State);
146+
// memory_order_relaxed: ordering is guaranteed by the mutex
147+
} while (result == 0 && !event->State.load(std::memory_order_relaxed));
143148

144149
if (result == 0 && event->AutoReset) {
145150
// We've only accquired the event if the wait succeeded
146-
event->State = false;
151+
// memory_order_release: Prevent overlapping/interleaved Set/Reset contexts
152+
event->State.store(false, std::memory_order_release);
147153
}
148154
} else if (event->AutoReset) {
149155
// It's an auto-reset event that's currently available;
150156
// we need to stop anyone else from using it
151157
result = 0;
152-
event->State = false;
158+
// memory_order_release: Prevent overlapping/interleaved Set/Reset contexts
159+
event->State.store(false, std::memory_order_release);
153160
}
154161
// Else we're trying to obtain a manual reset event with a signaled state;
155162
// don't do anything
@@ -158,21 +165,19 @@ namespace neosmart {
158165
}
159166

160167
int WaitForEvent(neosmart_event_t event, uint64_t milliseconds) {
161-
int tempResult;
162-
if (milliseconds == 0) {
163-
tempResult = pthread_mutex_trylock(&event->Mutex);
164-
if (tempResult == EBUSY) {
165-
return WAIT_TIMEOUT;
166-
}
168+
// Optimization: bypass acquiring the event lock if the state atomic is unavailable.
169+
// memory_order_relaxed: This is just an optimization, it's OK to be biased towards a stale
170+
// value here, and preferable to synchronizing CPU caches to get a more accurate result.
171+
if (milliseconds == 0 && !event->State.load(std::memory_order_relaxed)) {
172+
return WAIT_TIMEOUT;
167173
} else {
168-
tempResult = pthread_mutex_lock(&event->Mutex);
174+
int result = pthread_mutex_lock(&event->Mutex);
175+
assert(result == 0);
169176
}
170177

171-
assert(tempResult == 0);
172-
173178
int result = UnlockedWaitForEvent(event, milliseconds);
174179

175-
tempResult = pthread_mutex_unlock(&event->Mutex);
180+
int tempResult = pthread_mutex_unlock(&event->Mutex);
176181
assert(tempResult == 0);
177182

178183
return result;
@@ -344,7 +349,8 @@ namespace neosmart {
344349
int result = pthread_mutex_lock(&event->Mutex);
345350
assert(result == 0);
346351

347-
event->State = true;
352+
// memory_order_release: Unblock threads waiting for the event
353+
event->State.store(true, std::memory_order_release);
348354

349355
// Depending on the event type, we either trigger everyone or only one
350356
if (event->AutoReset) {
@@ -369,8 +375,6 @@ namespace neosmart {
369375
continue;
370376
}
371377

372-
event->State = false;
373-
374378
if (i->Waiter->WaitAll) {
375379
--i->Waiter->Status.EventsLeft;
376380
assert(i->Waiter->Status.EventsLeft >= 0);
@@ -391,14 +395,18 @@ namespace neosmart {
391395

392396
event->RegisteredWaits.pop_front();
393397

398+
// memory_order_release: Prevent overlapping of sequential Set/Reset states.
399+
event->State.store(false, std::memory_order_release);
400+
394401
result = pthread_mutex_unlock(&event->Mutex);
395402
assert(result == 0);
396403

397404
return 0;
398405
}
399406
#endif // WFMO
400-
// event->State can be false if compiled with WFMO support
401-
if (event->State) {
407+
// event->State can be false if compiled with WFMO support
408+
// memory_order_relaxed: ordering is ensured by the mutex
409+
if (event->State.load(std::memory_order_relaxed)) {
402410
result = pthread_mutex_unlock(&event->Mutex);
403411
assert(result == 0);
404412

@@ -407,7 +415,7 @@ namespace neosmart {
407415

408416
return 0;
409417
}
410-
} else {
418+
} else { // this is a manual reset event
411419
#ifdef WFMO
412420
for (size_t i = 0; i < event->RegisteredWaits.size(); ++i) {
413421
neosmart_wfmo_info_t info = &event->RegisteredWaits[i];
@@ -463,7 +471,9 @@ namespace neosmart {
463471
int result = pthread_mutex_lock(&event->Mutex);
464472
assert(result == 0);
465473

466-
event->State = false;
474+
// memory_order_release: Prevent sequential Set/Reset calls from overlapping, this seems to
475+
// be required per https://old.reddit.com/r/cpp/comments/g84bzv/c/fpua2yq/
476+
event->State.store(false, std::memory_order_release);
467477

468478
result = pthread_mutex_unlock(&event->Mutex);
469479
assert(result == 0);
@@ -493,7 +503,7 @@ namespace neosmart {
493503
#endif
494504
} // namespace neosmart
495505

496-
#else //_WIN32
506+
#else // _WIN32
497507

498508
#include <Windows.h>
499509
#include "pevents.h"

0 commit comments

Comments
 (0)