HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
spinRWMutex.h
Go to the documentation of this file.
1 //
2 // Copyright 2022 Pixar
3 //
4 // Licensed under the terms set forth in the LICENSE.txt file available at
5 // https://openusd.org/license.
6 //
7 #ifndef PXR_BASE_TF_SPIN_RW_MUTEX_H
8 #define PXR_BASE_TF_SPIN_RW_MUTEX_H
9 
10 #include "pxr/pxr.h"
11 #include "pxr/base/tf/api.h"
12 
13 #include "pxr/base/arch/hints.h"
15 
16 #include <atomic>
17 #include <utility>
18 
20 
21 /// \class TfSpinRWMutex
22 ///
23 /// This class implements a readers-writer spin lock that emphasizes throughput
24 /// when there is light contention or moderate contention dominated by readers.
25 /// Like all spin locks, significant contention performs poorly; consider a
26 /// different algorithm design or synchronization strategy in that case.
27 ///
28 /// In the best case, acquiring a read lock is an atomic add followed by a
29 /// conditional branch, and acquiring a write lock is an atomic bitwise-or
30 /// followed by a conditional branch.
31 ///
32 /// When contended by only readers, acquiring a read lock is the same: an atomic
33 /// add followed by a conditional branch. Of course the shared cache line being
34 /// concurrently read and modified will affect performance.
35 ///
36 /// In the worst case, acquiring a read lock does the atomic add and conditional
37 /// branch, but the condition shows writer activity, so the add must be undone
38 /// by a subtraction, and then the thread must wait to see no writer activity
39 /// before trying again.
40 ///
41 /// Similarly in the worst case for acquiring a write lock, the thread does the
42 /// atomic bitwise-or, but sees another active writer, and then must wait to see
43 /// no writer activity before trying again. Once the bitwise-or is done
44 /// successfully, then the writer must wait for any pending readers to clear out
45 /// before it can proceed.
46 ///
47 /// This class provides a nested TfSpinRWMutex::ScopedLock that makes it easy to
48 /// acquire locks, upgrade reader to writer, downgrade writer to reader, and
49 /// have those locks automatically release when the ScopedLock is destroyed.
50 ///
52 {
53  static constexpr int OneReader = 2;
54  static constexpr int WriterFlag = 1;
55 
56 public:
57 
58  /// Construct a mutex, initially unlocked.
59  TfSpinRWMutex() : _lockState(0) {}
60 
61  /// Tag type for constructing a ScopedLock associated with a mutex but not
62  /// yet acquired. Use with TfSpinRWMutex::deferAcquire.
63  struct DeferAcquire {};
64 
65  /// Tag value for deferred-acquisition ScopedLock construction.
66  static constexpr DeferAcquire deferAcquire {};
67 
68  /// Scoped lock utility class. API modeled roughly after
69  /// tbb::spin_rw_mutex::scoped_lock.
70  struct ScopedLock {
71 
72  /// Construct a scoped lock for mutex \p m and acquire either a read or
73  /// a write lock depending on \p write.
74  explicit ScopedLock(TfSpinRWMutex &m, bool write=true)
75  : _mutex(&m)
76  , _acqState(_NotAcquired) {
77  Acquire(write);
78  }
79 
80  /// Construct a scoped lock associated with mutex \p m but not yet
81  /// acquired. Use Acquire(), AcquireRead(), AcquireWrite(), or any
82  /// TryAcquire variant to acquire the lock.
84  : _mutex(&m), _acqState(_NotAcquired) {}
85 
86  /// Construct a scoped lock not associated with a \p mutex.
87  ScopedLock() : _mutex(nullptr), _acqState(_NotAcquired) {}
88 
89  /// Construct a new lock taking the \p other lock's mutex association
90  /// and acquisition state. Leave \p other not associated with a mutex.
91  ScopedLock(ScopedLock &&other) noexcept
92  : _mutex(std::exchange(other._mutex, nullptr))
93  , _acqState(std::exchange(other._acqState, _NotAcquired)) {}
94 
95  /// If \p this is not the same object as \p other, Release(), take the
96  /// \p other lock's mutex association and acquisition state, and leave
97  /// \p other not associated with a mutex. If \p this is the same object
98  /// as \p other, do nothing. In either case, return \p *this.
99  ScopedLock &operator=(ScopedLock &&other) noexcept {
100  if (this != &other) {
101  Release();
102  _mutex = std::exchange(other._mutex, nullptr);
103  _acqState = std::exchange(other._acqState, _NotAcquired);
104  }
105  return *this;
106  }
107 
108  /// If this scoped lock is acquired for either read or write, Release()
109  /// it.
111  Release();
112  }
113 
114  /// If the current scoped lock is acquired, Release() it, then associate
115  /// this lock with \p m and acquire either a read or a write lock,
116  /// depending on \p write.
117  void Acquire(TfSpinRWMutex &m, bool write=true) {
118  Release();
119  _mutex = &m;
120  Acquire(write);
121  }
122 
123  /// Acquire either a read or write lock on this lock's associated mutex
124  /// depending on \p write. This lock must be associated with a mutex
125  /// (typically by construction or by a call to Acquire() that takes a
126  /// mutex). This lock must not already be acquired when calling
127  /// Acquire().
128  void Acquire(bool write=true) {
129  if (write) {
130  AcquireWrite();
131  }
132  else {
133  AcquireRead();
134  }
135  }
136 
137  /// If the current scoped lock is acquired, Release() it, then associate
138  /// this lock with \p m and try to acquire either a read or a write
139  /// lock, depending on \p write. Return true if successfully acquired,
140  /// false if not.
141  bool TryAcquire(TfSpinRWMutex &m, bool write=true) {
142  Release();
143  _mutex = &m;
144  return TryAcquire(write);
145  }
146 
147  /// Try to acquire either a read or a write lock on this lock's
148  /// associated mutex. The lock must not already be acquired when
149  /// calling \p TryAcquire(). Return true if the lock was successfully
150  /// acquired, false if not.
151  bool TryAcquire(bool write=true) {
152  return write ? TryAcquireWrite() : TryAcquireRead();
153  }
154 
155  /// Release the currently required lock on the associated mutex. If
156  /// this lock is not currently acquired, silently do nothing.
157  void Release() {
158  switch (_acqState) {
159  default:
160  case _NotAcquired:
161  break;
162  case _ReadAcquired:
163  _ReleaseRead();
164  break;
165  case _WriteAcquired:
166  _ReleaseWrite();
167  break;
168  };
169  }
170 
171  /// Acquire a read lock on this lock's associated mutex. This lock must
172  /// not already be acquired when calling \p AcquireRead().
173  void AcquireRead() {
174  TF_DEV_AXIOM(_mutex);
175  TF_DEV_AXIOM(_acqState == _NotAcquired);
176  _mutex->AcquireRead();
177  _acqState = _ReadAcquired;
178  }
179 
180  /// Try to acquire a read lock on this lock's associated mutex. The
181  /// lock must not already be acquired when calling \p TryAcquireRead().
182  /// Return true if the lock was successfully acquired, false if not.
183  bool TryAcquireRead() {
184  TF_DEV_AXIOM(_mutex);
185  TF_DEV_AXIOM(_acqState == _NotAcquired);
186  if (_mutex->TryAcquireRead()) {
187  _acqState = _ReadAcquired;
188  return true;
189  }
190  return false;
191  }
192 
193  /// Acquire a write lock on this lock's associated mutex. This lock
194  /// must not already be acquired when calling \p AcquireWrite().
195  void AcquireWrite() {
196  TF_DEV_AXIOM(_mutex);
197  TF_DEV_AXIOM(_acqState == _NotAcquired);
198  _mutex->AcquireWrite();
199  _acqState = _WriteAcquired;
200  }
201 
202  /// Try to acquire a write lock on this lock's associated mutex without
203  /// waiting for other writers, but waiting for any currently active
204  /// readers to release. The lock must not already be acquired when
205  /// calling \p TryAcquireWrite(). Return true if the lock was
206  /// successfully acquired (no other writer was active), false if not.
207  /// Note: if readers are present but no other writer is active, this
208  /// call blocks until those readers have released.
210  TF_DEV_AXIOM(_mutex);
211  TF_DEV_AXIOM(_acqState == _NotAcquired);
212  if (_mutex->TryAcquireWrite()) {
213  _acqState = _WriteAcquired;
214  return true;
215  }
216  return false;
217  }
218 
219  /// Try to acquire a write lock on this lock's associated mutex only if
220  /// the mutex is in the fully released state (no readers, no writers).
221  /// The lock must not already be acquired when calling
222  /// \p TryAcquireWriteIfReleased(). Return true if the lock was
223  /// successfully acquired, false if not. Never blocks.
225  TF_DEV_AXIOM(_mutex);
226  TF_DEV_AXIOM(_acqState == _NotAcquired);
227  if (_mutex->TryAcquireWriteIfReleased()) {
228  _acqState = _WriteAcquired;
229  return true;
230  }
231  return false;
232  }
233 
234  /// Change this lock's acquisition state from a read lock to a write
235  /// lock. This lock must already be acquired for reading. Return true
236  /// if the upgrade occurred without releasing the read lock, false if it
237  /// was released.
239  TF_DEV_AXIOM(_mutex);
240  TF_DEV_AXIOM(_acqState == _ReadAcquired);
241  bool result = _mutex->UpgradeToWriter();
242  _acqState = _WriteAcquired;
243  return result;
244  }
245 
246  /// Change this lock's acquisition state from a write lock to a read
247  /// lock. This lock must already be acquired for writing. Return true
248  /// if the downgrade occurred without releasing the write in the
249  /// interim, false if it was released and other writers may have
250  /// intervened.
252  TF_DEV_AXIOM(_mutex);
253  TF_DEV_AXIOM(_acqState == _WriteAcquired);
254  _acqState = _ReadAcquired;
255  return _mutex->DowngradeToReader();
256  }
257 
258  private:
259 
260  // Acquisition states.
261  static constexpr int _NotAcquired = 0;
262  static constexpr int _ReadAcquired = 1;
263  static constexpr int _WriteAcquired = 2;
264 
265  void _ReleaseRead() {
266  TF_DEV_AXIOM(_acqState == _ReadAcquired);
267  _mutex->ReleaseRead();
268  _acqState = _NotAcquired;
269  }
270 
271  void _ReleaseWrite() {
272  TF_DEV_AXIOM(_acqState == _WriteAcquired);
273  _mutex->ReleaseWrite();
274  _acqState = _NotAcquired;
275  }
276 
277  TfSpinRWMutex *_mutex;
278  int _acqState; // _NotAcquired (0),
279  // _ReadAcquired (1),
280  // _WriteAcquired (2)
281  };
282 
283  /// Attempt to acquire a read lock on this mutex without waiting for
284  /// writers. This thread must not already hold a lock on this mutex (either
285  /// read or write). Return true if the lock is acquired, false otherwise.
286  inline bool TryAcquireRead() {
287  // Optimistically increment the reader count.
288  if (ARCH_LIKELY(!(_lockState.fetch_add(
289  OneReader, std::memory_order_acquire) &
290  WriterFlag))) {
291  // We incremented the reader count and observed no writer activity,
292  // we have a read lock.
293  return true;
294  }
295  // Otherwise there's writer activity. Undo the increment and return
296  // false. Release ordering ensures a waiting writer can observe this
297  // reader count drop to zero.
298  _lockState.fetch_sub(OneReader, std::memory_order_release);
299  return false;
300  }
301 
302  /// Acquire a read lock on this mutex. This thread must not already hold a
303  /// lock on this mutex (either read or write). Consider calling
304  /// DowngradeToReader() if this thread holds a write lock.
305  inline void AcquireRead() {
306  while (true) {
307  if (TryAcquireRead()) {
308  return;
309  }
310  // There's writer activity. Wait to see no writer activity and
311  // retry.
312  _WaitForWriter();
313  }
314  }
315 
316  /// Release this thread's read lock on this mutex.
317  inline void ReleaseRead() {
318  // Release ordering ensures the writer waiting in _WaitForReaders can
319  // observe this reader count drop to zero.
320  _lockState.fetch_sub(OneReader, std::memory_order_release);
321  }
322 
323  /// Attempt to acquire a write lock on this mutex without waiting for other
324  /// writers, but waiting for any currently active readers to release. This
325  /// thread must not already hold a lock on this mutex (either read or
326  /// write). Return true if the lock is acquired (no other writer was
327  /// active), false otherwise. Note: if readers are present but no other
328  /// writer is active, this call will block until those readers have
329  /// released.
330  inline bool TryAcquireWrite() {
331  int state = _lockState.fetch_or(WriterFlag, std::memory_order_acquire);
332  if (!(state & WriterFlag)) {
333  // We set the flag, wait for readers.
334  if (state != 0) {
335  // Wait for pending readers.
336  _WaitForReaders();
337  }
338  return true;
339  }
340  return false;
341  }
342 
343  /// Attempt to acquire a write lock on this mutex only if the mutex is in
344  /// the fully released state (no readers, no writers). This thread must not
345  /// already hold a lock on this mutex (either read or write). Return true
346  /// if the lock is acquired, false otherwise. Never blocks.
348  int expected = 0;
349  return _lockState.compare_exchange_strong(
350  expected, WriterFlag,
351  std::memory_order_acquire,
352  std::memory_order_relaxed);
353  }
354 
355  /// Acquire a write lock on this mutex. This thread must not already hold a
356  /// lock on this mutex (either read or write). Consider calling
357  /// UpgradeToWriter() if this thread holds a read lock.
358  void AcquireWrite() {
359  // Attempt to acquire -- if we fail then wait to see no other writer and
360  // retry.
361  while (true) {
362  if (TryAcquireWrite()) {
363  return;
364  }
365  _WaitForWriter();
366  }
367  }
368 
369  /// Release this thread's write lock on this mutex.
370  inline void ReleaseWrite() {
371  _lockState.fetch_and(~WriterFlag, std::memory_order_release);
372  }
373 
374  /// Upgrade this thread's lock on this mutex (which must be a read lock) to
375  /// a write lock. Return true if the upgrade is done "atomically" meaning
376  /// that the read lock was not released (and thus no other writer could have
377  /// acquired the lock in the interim). Return false if this lock was
378  /// released and thus another writer could have taken the lock in the
379  /// interim.
381  // This thread owns a read lock, attempt to upgrade to write lock. If
382  // we do so without an intervening writer, return true, otherwise return
383  // false.
384  const auto acquire = std::memory_order_acquire;
385  const auto release = std::memory_order_release;
386  bool atomic = true;
387  while (true) {
388  int state = _lockState.fetch_or(WriterFlag, acquire);
389  if (!(state & WriterFlag)) {
390  // We set the flag, release our reader count and wait for any
391  // other pending readers. Release ordering on the fetch_sub
392  // pairs with the acquire in _WaitForReaders.
393  if (_lockState.fetch_sub(
394  OneReader, release) != (OneReader | WriterFlag)) {
395  _WaitForReaders();
396  }
397  return atomic;
398  }
399  // There was other writer activity -- wait for it to clear, then
400  // retry.
401  atomic = false;
402  _WaitForWriter();
403  }
404  }
405 
406  /// Downgrade this mutex, which must be locked for write by this thread, to
407  /// being locked for read by this thread. Return true if the downgrade
408  /// happened "atomically", meaning that the write lock was not released (and
409  /// thus possibly acquired by another thread). This implementation
410  /// currently always returns true.
412  // Simultaneously add a reader count and clear the writer bit. Since we
413  // own the WriterFlag (=1) and we want to bump the reader count by
414  // OneReader (=2) we can achieve this by adding OneReader and then
415  // clearing WriterFlag. But that's the same as adding 2 and subtracting
416  // 1, so we can do this in a single step by just adding 1. We write
417  // this as 'OneReader - 1' so it's clear this isn't a simple increment.
418  //
419  // acq_rel: release for the write side (make protected writes visible to
420  // subsequent readers), acquire for the read side (establish
421  // happens-before for subsequent reads of protected data).
422  _lockState.fetch_add(OneReader - 1, std::memory_order_acq_rel);
423  return true;
424  }
425 
426 private:
427  friend class TfBigRWMutex;
428 
429  // Helpers for staged-acquire-write that BigRWMutex uses.
430  enum _StagedAcquireWriteState {
431  _StageNotAcquired,
432  _StageAcquiring,
433  _StageAcquired
434  };
435 
436  // This API lets TfBigRWMutex acquire a write lock step-by-step so that it
437  // can begin acquiring write locks on several mutexes without waiting
438  // serially for pending readers to complete. Call _StagedAcquireWriteStep
439  // with _StageNotAcquired initially, and save the returned value. Continue
440  // repeatedly calling _StagedAcquireWriteStep, passing the previously
441  // returned value until this function returns _StageAcquired. At this
442  // point the write lock is acquired.
443  _StagedAcquireWriteState
444  _StagedAcquireWriteStep(_StagedAcquireWriteState curState) {
445  int state;
446  switch (curState) {
447  case _StageNotAcquired:
448  state = _lockState.fetch_or(WriterFlag, std::memory_order_acquire);
449  if (!(state & WriterFlag)) {
450  // We set the flag. If there were no readers we're done,
451  // otherwise we'll have to wait for them, next step.
452  return state == 0 ? _StageAcquired : _StageAcquiring;
453  }
454  // Other writer activity, must retry next step.
455  return _StageNotAcquired;
456  case _StageAcquiring:
457  // We have set the writer flag but must wait to see no readers.
458  _WaitForReaders();
459  return _StageAcquired;
460  case _StageAcquired:
461  default:
462  return _StageAcquired;
463  };
464  }
465 
466  TF_API void _WaitForReaders() const;
467  TF_API void _WaitForWriter() const;
468 
469  std::atomic<int> _lockState;
470 };
471 
473 
474 #endif // PXR_BASE_TF_SPIN_RW_MUTEX_H
475 
#define ARCH_LIKELY(x)
Definition: hints.h:29
ScopedLock()
Construct a scoped lock not associated with a mutex.
Definition: spinRWMutex.h:87
bool TryAcquireWrite()
Definition: spinRWMutex.h:330
#define TF_API
Definition: api.h:23
#define PXR_NAMESPACE_OPEN_SCOPE
Definition: pxr.h:73
bool TryAcquireWriteIfReleased()
Definition: spinRWMutex.h:347
ScopedLock(TfSpinRWMutex &m, TfSpinRWMutex::DeferAcquire)
Definition: spinRWMutex.h:83
bool DowngradeToReader()
Definition: spinRWMutex.h:411
**But if you need a result
Definition: thread.h:622
ScopedLock(ScopedLock &&other) noexcept
Definition: spinRWMutex.h:91
bool TryAcquire(TfSpinRWMutex &m, bool write=true)
Definition: spinRWMutex.h:141
ScopedLock & operator=(ScopedLock &&other) noexcept
Definition: spinRWMutex.h:99
void Acquire(bool write=true)
Definition: spinRWMutex.h:128
#define TF_DEV_AXIOM(cond)
bool TryAcquire(bool write=true)
Definition: spinRWMutex.h:151
void AcquireWrite()
Definition: spinRWMutex.h:358
bool UpgradeToWriter()
Definition: spinRWMutex.h:380
void AcquireRead()
Definition: spinRWMutex.h:305
bool TryAcquireRead()
Definition: spinRWMutex.h:286
static constexpr DeferAcquire deferAcquire
Tag value for deferred-acquisition ScopedLock construction.
Definition: spinRWMutex.h:66
VULKAN_HPP_CONSTEXPR_14 VULKAN_HPP_INLINE T exchange(T &obj, U &&newValue)
Definition: vulkan_raii.hpp:25
TfSpinRWMutex()
Construct a mutex, initially unlocked.
Definition: spinRWMutex.h:59
#define PXR_NAMESPACE_CLOSE_SCOPE
Definition: pxr.h:74
void ReleaseWrite()
Release this thread's write lock on this mutex.
Definition: spinRWMutex.h:370
void ReleaseRead()
Release this thread's read lock on this mutex.
Definition: spinRWMutex.h:317
state
Definition: core.h:2289
ScopedLock(TfSpinRWMutex &m, bool write=true)
Definition: spinRWMutex.h:74
void Acquire(TfSpinRWMutex &m, bool write=true)
Definition: spinRWMutex.h:117