CeresEngine 0.2.0
A game development framework
Loading...
Searching...
No Matches
AsyncMutex.hpp
Go to the documentation of this file.
1//
2// CeresEngine - A game development framework
3//
4// Created by Rogiel Sulzbach.
5// Copyright (c) 2018-2022 Rogiel Sulzbach. All rights reserved.
6//
7
8#pragma once
9
10#include "ExecutionContext.hpp"
11#include "Threading.hpp"
12
15
18
19namespace CeresEngine {
20
30 template<typename ExecutorType = InlineExecutor> class TAsyncMutex {
31 public:
32 class Lock;
33 class LockOperation;
35
36 private:
37 friend class LockOperation;
38
41
42 static constexpr std::uintptr_t kNotLocked = 1;
43 static constexpr std::uintptr_t kLockedNoWaiters = 0;
44
45 // NOTE: assumes == reinterpret_cast<std::uintptr_t>(static_cast<void*>(nullptr))
46
57
62
63 public:
66
72
78 [[maybe_unused]] const std::uintptr_t state = mState.load(std::memory_order_relaxed);
80 CE_ASSERT(mWaiters == nullptr);
81 }
82
90 // Try to atomically transition from nullptr (not-locked) -> this (locked-no-waiters).
91 std::uintptr_t oldState = kNotLocked;
92 return mState.compare_exchange_strong(oldState, kLockedNoWaiters, std::memory_order_acquire, std::memory_order_relaxed);
93 }
94
96 [[nodiscard]] bool try_lock() noexcept { return tryLock(); }
97
109
123
131 void unlock() {
132 CE_ASSERT(mState.load(std::memory_order_relaxed) != kNotLocked);
133
135 if(waitersHead == nullptr) {
136 std::uintptr_t oldState = kLockedNoWaiters;
137 const bool releasedLock = mState.compare_exchange_strong(oldState, kNotLocked, std::memory_order_release, std::memory_order_relaxed);
138 if(releasedLock) {
139 return;
140 }
141
142 // At least one new waiter.
143 // Acquire the list of new waiter operations atomically.
144 oldState = mState.exchange(kLockedNoWaiters, std::memory_order_acquire);
146
147 // Transfer the list to mWaiters, reversing the list in the process so
148 // that the head of the list is the first to be resumed.
149 LockOperation* next = reinterpret_cast<LockOperation*>(oldState);
150 do {
151 LockOperation* const temporaryNext = next->mNext;
152 next->mNext = waitersHead;
153 waitersHead = next;
154 next = temporaryNext;
155 } while(next != nullptr);
156 }
157 CE_ASSERT(waitersHead != nullptr);
158
160
161 // Resume the waiter.
162 // This will pass the ownership of the lock on to that operation/coroutine.
163 dispatch(mExecutor, [awaiter = waitersHead->mAwaiter]() mutable { awaiter.resume(); });
164 }
165
166 public:
174 class Lock {
175 public:
176 explicit Lock(TAsyncMutex& mutex, std::adopt_lock_t) noexcept : mMutex(&mutex) {}
177
178 Lock(Lock&& other) noexcept : mMutex(std::exchange(other.mMutex, nullptr)) {}
179 Lock& operator=(Lock&& other) noexcept {
180 release();
181 mMutex = std::exchange(mMutex, nullptr);
182 return *this;
183 }
184
185 Lock(const Lock& other) = delete;
186 Lock& operator=(const Lock& other) = delete;
187
188 // Releases the lock.
189 ~Lock() { release(); }
190
191 public:
192 inline void unlock() { release(); }
193
194 private:
195 void release() {
196 if(mMutex != nullptr) {
197 mMutex->unlock();
198 }
199 }
200
202 };
203
205 friend class TAsyncMutex;
206
207 private:
211
215
216 protected:
219
220 public:
221 explicit LockOperation(TAsyncMutex& mutex) noexcept : mMutex(mutex) {}
222
223 public: // Coroutine interface
224 [[nodiscard]] bool await_ready() const noexcept { return false; }
227
228 std::uintptr_t oldState = mMutex.mState.load(std::memory_order_acquire);
229 while(true) {
231 if(mMutex.mState.compare_exchange_weak(oldState, TAsyncMutex::kLockedNoWaiters, std::memory_order_acquire, std::memory_order_relaxed)) {
232 // Acquired lock, don't suspend.
233 return false;
234 }
235 } else {
236 // Try to push this operation onto the head of the waiter stack.
237 mNext = reinterpret_cast<LockOperation*>(oldState);
238 if(mMutex.mState.compare_exchange_weak(oldState, reinterpret_cast<std::uintptr_t>(this), std::memory_order_release, std::memory_order_relaxed)) {
239 // Queued operation to waiters list, suspend now.
240 return true;
241 }
242 }
243 }
244 }
246 };
247
249 public:
251
252 [[nodiscard]] Lock await_resume() const noexcept { return Lock{this->mMutex, std::adopt_lock}; }
253 };
254 };
255
256 extern template class TAsyncMutex<>;
257
260
261 template<typename ExecutorType> class TAsyncSharedMutex;
262
263 template<typename ExecutorType = InlineExecutor> class TAsyncSharedMutex {
264 public:
265 class Lock;
266 class LockOperation;
267
268 private:
269 friend class LockOperation;
270
271 enum class State {
272 Unlocked,
273 LockedShared,
274 LockedExclusive
275 };
276
279
281 State mState = State::Unlocked;
282
284 uint64_t mSharedUsers = 0;
285
288 uint64_t mExclusiveWaiters = 0;
289
290 LockOperation* mHeadWaiter = nullptr;
291 LockOperation* mTailWaiter = nullptr;
292
293 public:
295 explicit TAsyncSharedMutex() {}
296
301 explicit TAsyncSharedMutex(ExecutorType&& executor) : mExecutor(std::forward<ExecutorType>(executor)) {}
303
308
312 [[nodiscard]] LockOperation lock_shared() { return LockOperation{*this, false}; }
313
315 [[nodiscard]] LockOperation lock() { return LockOperation{*this, true}; }
316
319 auto try_lock_shared() -> bool {
320 // To acquire the shared lock the state must be one of two states:
321 // 1) unlocked
322 // 2) shared locked with zero exclusive waiters
323 // Zero exclusive waiters prevents exclusive starvation if shared locks are
324 // always continuously happening.
325
326 UniqueLock<Mutex> lk{mMutex};
327 return try_lock_shared_locked(lk);
328 }
329
332 [[nodiscard]] bool try_lock() {
333 // To acquire the exclusive lock the state must be unlocked.
334 UniqueLock<Mutex> lk{mMutex};
335 return try_lock_locked(lk);
336 }
337
346 UniqueLock<Mutex> lk{mMutex};
347 --mSharedUsers;
348
349 // Only wake waiters from shared state if all shared users have completed.
350 if(mSharedUsers == 0) {
351 if(mHeadWaiter != nullptr) {
352 wakeWaiters(lk);
353 } else {
354 mState = State::Unlocked;
355 }
356 }
357 }
358
364 void unlock() {
365 UniqueLock<Mutex> lk{mMutex};
366 if(mHeadWaiter != nullptr) {
367 wakeWaiters(lk);
368 } else {
369 mState = State::Unlocked;
370 }
371 }
372
373 public:
378 class Lock {
379 public:
380 Lock(TAsyncSharedMutex& sm, const bool exclusive) : mMutex(&sm), mExclusive(exclusive) {}
381
383 ~Lock() { unlock(); }
384
385 Lock(const Lock&) = delete;
386 Lock& operator=(const Lock&) = delete;
387
388 Lock(Lock&& other) noexcept : mMutex(std::exchange(other.mMutex, nullptr)), mExclusive(other.mExclusive) {}
389 Lock& operator=(Lock&& other) noexcept {
390 if(std::addressof(other) != this) {
391 mMutex = std::exchange(other.mMutex, nullptr);
392 mExclusive = other.mExclusive;
393 }
394 return *this;
395 }
396
398 void unlock() {
399 if(mMutex != nullptr) {
400 if(mExclusive) {
401 mMutex->unlock();
402 } else {
403 mMutex->unlock_shared();
404 }
405
406 mMutex = nullptr;
407 }
408 }
409
410 [[nodiscard]] bool isExclusive() const noexcept { return mExclusive; }
411
412 private:
413 TAsyncSharedMutex* mMutex{nullptr};
414 bool mExclusive{false};
415 };
416
418 friend class TAsyncSharedMutex;
419
420 private:
422 bool mExclusive = false;
424 LockOperation* mNext = nullptr;
425
426 public:
427 LockOperation(TAsyncSharedMutex& sm, const bool exclusive) : mMutex(sm), mExclusive(exclusive) {}
428
430 if(mExclusive) {
431 return mMutex.try_lock();
432 } else {
433 return mMutex.try_lock_shared();
434 }
435 }
436
439 // Its possible the lock has been released between await_ready() and await_suspend(), double
440 // check and make sure we are not going to suspend when nobody holds the lock.
441 if(mExclusive) {
442 if(mMutex.try_lock_locked(lk)) {
443 return false;
444 }
445 } else {
446 if(mMutex.try_lock_shared_locked(lk)) {
447 return false;
448 }
449 }
450
451 // For sure the lock is currently held in a manner that it cannot be acquired, suspend ourself
452 // at the end of the waiter list.
453
454 if(mMutex.mTailWaiter == nullptr) {
455 mMutex.mHeadWaiter = this;
456 mMutex.mTailWaiter = this;
457 } else {
458 mMutex.mTailWaiter->mNext = this;
459 mMutex.mTailWaiter = this;
460 }
461
462 // If this is an exclusive lock acquire then mark it as so so that shared locks after this
463 // exclusive one will also suspend so this exclusive lock doens't get starved.
464 if(mExclusive) {
465 ++mMutex.mExclusiveWaiters;
466 }
467
468 mAwaitingCoroutine = awaitingCoroutine;
469 return true;
470 }
471 [[nodiscard]] Lock await_resume() noexcept { return Lock{mMutex, mExclusive}; }
472 };
473
474 private:
476 if(mState == State::Unlocked) {
477 // If the shared mutex is unlocked put it into shared mode and add ourself as using the lock.
478 mState = State::LockedShared;
479 ++mSharedUsers;
480 lk.unlock();
481 return true;
482 } else if(mState == State::LockedShared && mExclusiveWaiters == 0) {
483 // If the shared mutex is in a shared locked state and there are no exclusive waiters
484 // the add ourself as using the lock.
485 ++mSharedUsers;
486 lk.unlock();
487 return true;
488 }
489
490 // If the lock is in shared mode but there are exclusive waiters then we will also wait so
491 // the writers are not starved.
492 // If the lock is in exclusive mode already then we need to wait.
493
494 return false;
495 }
496
498 if(mState == State::Unlocked) {
499 mState = State::LockedExclusive;
500 lk.unlock();
501 return true;
502 }
503 return false;
504 }
505
507 // First determine what the next lock state will be based on the first waiter.
508 if(mHeadWaiter->mExclusive) {
509 // If its exclusive then only this waiter can be woken up.
510 mState = State::LockedExclusive;
511 LockOperation* waiterToResume = mHeadWaiter;
512 mHeadWaiter = mHeadWaiter->mNext;
513 --mExclusiveWaiters;
514 if(mHeadWaiter == nullptr) {
515 mTailWaiter = nullptr;
516 }
517
518 // Since this is an exclusive lock waiting we can resume it directly.
519 lk.unlock();
520 dispatch(mExecutor, [handle = waiterToResume->mAwaitingCoroutine]() mutable { handle.resume(); });
521 } else {
522 // If its shared then we will scan forward and awake all shared waiters onto the given
523 // thread pool so they can run in parallel.
524 mState = State::LockedShared;
525 do {
526 LockOperation* waiterToResume = mHeadWaiter;
527 mHeadWaiter = mHeadWaiter->mNext;
528 if(mHeadWaiter == nullptr) {
529 mTailWaiter = nullptr;
530 }
531 ++mSharedUsers;
532
533 dispatch(mExecutor, [handle = waiterToResume->mAwaitingCoroutine]() mutable { handle.resume(); });
534 } while(mHeadWaiter != nullptr && !mHeadWaiter->mExclusive);
535
536 // Cannot unlock until the entire set of shared waiters has
537 // been traversed. I think this makes more sense than
538 // allocating space for all the shared waiters, unlocking,
539 // and then resuming in a batch?
540 lk.unlock();
541 }
542 }
543 };
544
545 extern template class TAsyncSharedMutex<>;
546
549
550} // namespace CeresEngine
#define CE_ASSERT(...)
Definition Macros.hpp:323
An object that holds onto a mutex lock for its lifetime and ensures that the mutex is unlocked when i...
Definition AsyncMutex.hpp:174
Lock & operator=(const Lock &other)=delete
~Lock()
Definition AsyncMutex.hpp:189
TAsyncMutex * mMutex
Definition AsyncMutex.hpp:201
void release()
Definition AsyncMutex.hpp:195
Lock(Lock &&other) noexcept
Definition AsyncMutex.hpp:178
Lock(TAsyncMutex &mutex, std::adopt_lock_t) noexcept
Definition AsyncMutex.hpp:176
Lock(const Lock &other)=delete
void unlock()
Definition AsyncMutex.hpp:192
Lock & operator=(Lock &&other) noexcept
Definition AsyncMutex.hpp:179
Definition AsyncMutex.hpp:204
LockOperation(TAsyncMutex &mutex) noexcept
Definition AsyncMutex.hpp:221
void await_resume() const noexcept
Definition AsyncMutex.hpp:245
LockOperation * mNext
The next LockOperation in the chain.
Definition AsyncMutex.hpp:210
CoroutineHandle mAwaiter
The coroutine handle to the awaiter that is waiting for the lock.
Definition AsyncMutex.hpp:214
TAsyncMutex & mMutex
The ´AsyncMutex` that the lock operations belongs to.
Definition AsyncMutex.hpp:218
bool await_suspend(const CoroutineHandle<> awaiter) noexcept
Definition AsyncMutex.hpp:225
bool await_ready() const noexcept
Definition AsyncMutex.hpp:224
Lock await_resume() const noexcept
Definition AsyncMutex.hpp:252
A mutex that can be locked asynchronously using 'co_await'.
Definition AsyncMutex.hpp:30
static constexpr std::uintptr_t kLockedNoWaiters
Definition AsyncMutex.hpp:43
ScopedLockOperation scopedLock() noexcept
Acquire a lock on the mutex asynchronously, returning an object that will call unlock() automatically...
Definition AsyncMutex.hpp:122
LockOperation lock() noexcept
Acquire a lock on the mutex asynchronously.
Definition AsyncMutex.hpp:108
~TAsyncMutex() noexcept
Destroys the mutex.
Definition AsyncMutex.hpp:77
TAsyncMutex() noexcept=default
Construct to a mutex that is not currently locked.
static constexpr std::uintptr_t kNotLocked
Definition AsyncMutex.hpp:42
bool tryLock() noexcept
Attempt to acquire a lock on the mutex without blocking.
Definition AsyncMutex.hpp:89
void unlock()
Unlock the mutex.
Definition AsyncMutex.hpp:131
Atomic< std::uintptr_t > mState
This field provides synchronisation for the mutex.
Definition AsyncMutex.hpp:56
ExecutorType mExecutor
This executor is for resuming multiple shared waiters.
Definition AsyncMutex.hpp:40
LockOperation * mWaiters
Linked list of async lock operations that are waiting to acquire the mutex.
Definition AsyncMutex.hpp:61
bool try_lock() noexcept
Attempt to acquire a lock on the mutex without blocking.
Definition AsyncMutex.hpp:96
A scoped RAII lock holder for a AsyncSharedMutex.
Definition AsyncMutex.hpp:378
Lock & operator=(Lock &&other) noexcept
Definition AsyncMutex.hpp:389
bool isExclusive() const noexcept
Definition AsyncMutex.hpp:410
Lock(TAsyncSharedMutex &sm, const bool exclusive)
Definition AsyncMutex.hpp:380
Lock & operator=(const Lock &)=delete
void unlock()
Unlocks the shared mutex prior to this lock going out of scope.
Definition AsyncMutex.hpp:398
Lock(Lock &&other) noexcept
Definition AsyncMutex.hpp:388
~Lock()
Unlocks the mutex upon this shared scoped lock destructing.
Definition AsyncMutex.hpp:383
LockOperation * mNext
Definition AsyncMutex.hpp:424
CoroutineHandle mAwaitingCoroutine
Definition AsyncMutex.hpp:423
LockOperation(TAsyncSharedMutex &sm, const bool exclusive)
Definition AsyncMutex.hpp:427
bool await_suspend(const CoroutineHandle<> awaitingCoroutine) noexcept
Definition AsyncMutex.hpp:437
TAsyncSharedMutex & mMutex
Definition AsyncMutex.hpp:421
friend class TAsyncSharedMutex
Definition AsyncMutex.hpp:418
bool await_ready() const noexcept
Definition AsyncMutex.hpp:429
bool mExclusive
Definition AsyncMutex.hpp:422
Lock await_resume() noexcept
Definition AsyncMutex.hpp:471
Definition AsyncMutex.hpp:263
LockOperation lock_shared()
Locks the mutex in a shared state.
Definition AsyncMutex.hpp:312
void unlock_shared()
Unlocks a single shared state user.
Definition AsyncMutex.hpp:345
TAsyncSharedMutex & operator=(const TAsyncSharedMutex &)=delete
bool try_lock()
Definition AsyncMutex.hpp:332
void wakeWaiters(UniqueLock< Mutex > &lk)
Definition AsyncMutex.hpp:506
friend class LockOperation
Definition AsyncMutex.hpp:269
TAsyncSharedMutex(const TAsyncSharedMutex &)=delete
LockOperation * mTailWaiter
Definition AsyncMutex.hpp:291
Mutex mMutex
Definition AsyncMutex.hpp:280
LockOperation * mHeadWaiter
Definition AsyncMutex.hpp:290
auto try_lock_shared() -> bool
Definition AsyncMutex.hpp:319
bool try_lock_shared_locked(UniqueLock< Mutex > &lk)
Definition AsyncMutex.hpp:475
State
Definition AsyncMutex.hpp:271
TAsyncSharedMutex()
Creates a new async shared mutex.
Definition AsyncMutex.hpp:295
TAsyncSharedMutex & operator=(TAsyncSharedMutex &&)=delete
TAsyncSharedMutex(ExecutorType &&executor)
Definition AsyncMutex.hpp:301
void unlock()
Unlocks the mutex from its exclusive state.
Definition AsyncMutex.hpp:364
bool try_lock_locked(UniqueLock< Mutex > &lk)
Definition AsyncMutex.hpp:497
TAsyncSharedMutex(TAsyncSharedMutex &&)=delete
uint64_t mExclusiveWaiters
The current number of exclusive waiters waiting to acquire the lock.
Definition AsyncMutex.hpp:288
LockOperation lock()
Locks the mutex in an exclusive state.
Definition AsyncMutex.hpp:315
ExecutorType mExecutor
This executor is for resuming multiple shared waiters.
Definition AsyncMutex.hpp:278
The class UniqueLock is a general-purpose mutex ownership wrapper allowing deferred locking,...
Definition Threading.hpp:270
Definition Application.hpp:19
std::coroutine_handle< T > CoroutineHandle
A type alias to the C++ standard library coroutine handle.
Definition Coroutine.hpp:26
std::mutex Mutex
The Mutex class is a synchronization primitive that can be used to protect shared data from being sim...
Definition Threading.hpp:73
struct CeresEngine::GLState state
std::atomic< T > Atomic
The Atomic template defines an atomic type.
Definition Atomic.hpp:16
constexpr size_t hash(const T &v)
Generates a hash for the provided type.
Definition Hash.hpp:25
Definition Span.hpp:668