From fd6152540e92418a433311b2e7790f7d0e21f170 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 7 Dec 2023 15:11:50 -0500 Subject: [PATCH 1/6] gh-111964: Add `_PyRWMutex` a "readers-writer" lock This adds `_PyRWMutex`, a "readers-writer" lock, which wil be used to serialize global stop-the-world pauses with per-interpreter pauses. --- Include/internal/pycore_lock.h | 24 +++++++ Modules/_testinternalcapi/test_lock.c | 93 +++++++++++++++++++++++++++ Python/lock.c | 86 +++++++++++++++++++++++++ 3 files changed, 203 insertions(+) diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h index 03ad1c9fd584b5..7cc7a8cc95ddb2 100644 --- a/Include/internal/pycore_lock.h +++ b/Include/internal/pycore_lock.h @@ -213,6 +213,30 @@ _PyOnceFlag_CallOnce(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg) return _PyOnceFlag_CallOnceSlow(flag, fn, arg); } +// A readers-writer (RW) lock. The lock supports multiple concurrent readers or +// a single writer. The lock is write-preferring: if a writer is waiting, then +// new readers will be blocked. This avoids starvation of writers. +// +// The low two bits store whether the lock is write-locked (_Py_LOCKED) and +// whether there are parked threads (_Py_HAS_PARKED). The remaining bits are +// used to store the number of readers. +// +// The design is optimized for simplicity of the implementation. The lock is +// not fair: if fairness is desired, use an additional PyMutex to serialize +// writers. The lock is also not reentrant. +typedef struct { + uintptr_t bits; +} _PyRWMutex; + +// Read lock +PyAPI_FUNC(void) _PyRWMutex_RLock(_PyRWMutex *rwmutex); +PyAPI_FUNC(void) _PyRWMutex_RUnlock(_PyRWMutex *rwmutex); + +// Write lock +PyAPI_FUNC(void) _PyRWMutex_Lock(_PyRWMutex *rwmutex); +PyAPI_FUNC(void) _PyRWMutex_Unlock(_PyRWMutex *rwmutex); + + #ifdef __cplusplus } #endif diff --git a/Modules/_testinternalcapi/test_lock.c b/Modules/_testinternalcapi/test_lock.c index 418f71c1441995..1dd3c736ad3f28 100644 --- a/Modules/_testinternalcapi/test_lock.c +++ b/Modules/_testinternalcapi/test_lock.c @@ -372,6 +372,98 @@ test_lock_once(PyObject *self, PyObject *obj) Py_RETURN_NONE; } +struct test_rwlock_data { + Py_ssize_t nthreads; + _PyRWMutex rw; + PyEvent step1; + PyEvent step2; + PyEvent step3; + PyEvent done; +}; + +static void +rdlock_thread(void *arg) +{ + struct test_rwlock_data *test_data = arg; + + // Acquire the lock in read mode + _PyRWMutex_RLock(&test_data->rw); + PyEvent_Wait(&test_data->step1); + _PyRWMutex_RUnlock(&test_data->rw); + + _PyRWMutex_RLock(&test_data->rw); + PyEvent_Wait(&test_data->step3); + _PyRWMutex_RUnlock(&test_data->rw); + + if (_Py_atomic_add_ssize(&test_data->nthreads, -1) == 1) { + _PyEvent_Notify(&test_data->done); + } +} +static void +wrlock_thread(void *arg) +{ + struct test_rwlock_data *test_data = arg; + + // First acquire the lock in write mode + _PyRWMutex_Lock(&test_data->rw); + PyEvent_Wait(&test_data->step2); + _PyRWMutex_Unlock(&test_data->rw); + + if (_Py_atomic_add_ssize(&test_data->nthreads, -1) == 1) { + _PyEvent_Notify(&test_data->done); + } +} + +static void +wait_until(uintptr_t *ptr, uintptr_t value) +{ + // wait up to two seconds for *ptr == value + int iters = 0; + uintptr_t bits; + do { + pysleep(10); + bits = _Py_atomic_load_uintptr(ptr); + iters++; + } while (bits != value && iters < 200); +} + +static PyObject * +test_lock_rwlock(PyObject *self, PyObject *obj) +{ + struct test_rwlock_data test_data = {.nthreads = 3}; + + // Start two readers + PyThread_start_new_thread(rdlock_thread, &test_data); + PyThread_start_new_thread(rdlock_thread, &test_data); + + // wait up to two seconds for the threads to attempt to read-lock "rw" + wait_until(&test_data.rw.bits, 8); + assert(test_data.rw.bits == 8); + + // start writer (while readers hold lock) + PyThread_start_new_thread(wrlock_thread, &test_data); + wait_until(&test_data.rw.bits, 10); + assert(test_data.rw.bits == 10); + + // readers release lock, writer should acquire it + _PyEvent_Notify(&test_data.step1); + wait_until(&test_data.rw.bits, 3); + assert(test_data.rw.bits == 3); + + // writer releases lock, readers acquire it + _PyEvent_Notify(&test_data.step2); + wait_until(&test_data.rw.bits, 8); + assert(test_data.rw.bits == 8); + + // readers release lock again + _PyEvent_Notify(&test_data.step3); + wait_until(&test_data.rw.bits, 0); + assert(test_data.rw.bits == 0); + + PyEvent_Wait(&test_data.done); + Py_RETURN_NONE; +} + static PyMethodDef test_methods[] = { {"test_lock_basic", test_lock_basic, METH_NOARGS}, {"test_lock_two_threads", test_lock_two_threads, METH_NOARGS}, @@ -380,6 +472,7 @@ static PyMethodDef test_methods[] = { _TESTINTERNALCAPI_BENCHMARK_LOCKS_METHODDEF {"test_lock_benchmark", test_lock_benchmark, METH_NOARGS}, {"test_lock_once", test_lock_once, METH_NOARGS}, + {"test_lock_rwlock", test_lock_rwlock, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/lock.c b/Python/lock.c index e9279f0b92a5e7..1883aff7b5e03c 100644 --- a/Python/lock.c +++ b/Python/lock.c @@ -353,3 +353,89 @@ _PyOnceFlag_CallOnceSlow(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg) v = _Py_atomic_load_uint8(&flag->v); } } + +#define _PyRWMutex_READER_SHIFT 2 + +void +_PyRWMutex_RLock(_PyRWMutex *rwmutex) +{ + uintptr_t bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); + for (;;) { + // If the lock is not write-locked and there is no writer waiting, then + // we can increment the reader count. + if ((bits & (_Py_LOCKED|_Py_HAS_PARKED)) == 0) { + uintptr_t newval = bits + (1 << _PyRWMutex_READER_SHIFT); + if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, + &bits, newval)) { + continue; + } + return; + } + + // Set _Py_HAS_PARKED if it's not already set. + if ((bits & _Py_HAS_PARKED) == 0) { + uintptr_t newval = bits | _Py_HAS_PARKED; + if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, + &bits, newval)) { + continue; + } + bits = newval; + } + + _PyParkingLot_Park(&rwmutex->bits, &bits, sizeof(bits), -1, NULL, 1); + bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); + } +} + +void +_PyRWMutex_RUnlock(_PyRWMutex *rwmutex) +{ + uintptr_t bits = _Py_atomic_add_uintptr(&rwmutex->bits, -(1 << _PyRWMutex_READER_SHIFT)); + bits -= (1 << _PyRWMutex_READER_SHIFT); + + if ((bits >> _PyRWMutex_READER_SHIFT) == 0 && (bits & _Py_HAS_PARKED)) { + _PyParkingLot_UnparkAll(&rwmutex->bits); + return; + } +} + +void +_PyRWMutex_Lock(_PyRWMutex *rwmutex) +{ + uintptr_t bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); + for (;;) { + // If there are no active readers and it's not already write-locked, + // then we can grab the lock. + if ((bits & ~_Py_HAS_PARKED) == 0) { + if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, + &bits, + bits | _Py_LOCKED)) { + return; + } + continue; + } + + if (!(bits & _Py_HAS_PARKED)) { + if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, + &bits, + bits | _Py_HAS_PARKED)) { + continue; + } + bits |= _Py_HAS_PARKED; + } + + _PyParkingLot_Park(&rwmutex->bits, &bits, sizeof(bits), -1, NULL, 1); + bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); + } +} + +void +_PyRWMutex_Unlock(_PyRWMutex *rwmutex) +{ + uintptr_t old_bits = _Py_atomic_exchange_uintptr(&rwmutex->bits, 0); + assert(old_bits >> _PyRWMutex_READER_SHIFT == 0); + + if ((old_bits & _Py_HAS_PARKED) != 0) { + _PyParkingLot_UnparkAll(&rwmutex->bits); + } +} From c6b9d62a84ef76c8b4a2e758120b18d58e632580 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 7 Dec 2023 16:36:21 -0500 Subject: [PATCH 2/6] Add comments and ASCII diagram based on review. Also, fix bug where `_PyRWMutex_Lock()` swapped the return/continue conditions leading to an extra iteration of the loop. --- Include/internal/pycore_lock.h | 17 +++++- Modules/_testinternalcapi/test_lock.c | 6 ++ Python/lock.c | 80 ++++++++++++++++----------- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h index 7cc7a8cc95ddb2..baaf78a9f18638 100644 --- a/Include/internal/pycore_lock.h +++ b/Include/internal/pycore_lock.h @@ -217,9 +217,20 @@ _PyOnceFlag_CallOnce(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg) // a single writer. The lock is write-preferring: if a writer is waiting, then // new readers will be blocked. This avoids starvation of writers. // -// The low two bits store whether the lock is write-locked (_Py_LOCKED) and -// whether there are parked threads (_Py_HAS_PARKED). The remaining bits are -// used to store the number of readers. +// The bitfield is structured as follows: +// +// N ... 2 1 0 +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | reader_count |P |WL| +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// +// The least significant bit (WL / _Py_WRITE_LOCKED) indicates if the mutex is +// write-locked. The next bit (PK/ _Py_HAS_PARKED) indicates if there are +// parked threads (either readers or writers). The remaining bits +// (reader_count) indicate the number of readers holding the lock. +// +// Note that reader_count must be zero if WL is set, and vice versa. The lock +// can only be held by readers or a writer, but not both. // // The design is optimized for simplicity of the implementation. The lock is // not fair: if fairness is desired, use an additional PyMutex to serialize diff --git a/Modules/_testinternalcapi/test_lock.c b/Modules/_testinternalcapi/test_lock.c index 1dd3c736ad3f28..83081f73a72f64 100644 --- a/Modules/_testinternalcapi/test_lock.c +++ b/Modules/_testinternalcapi/test_lock.c @@ -432,6 +432,12 @@ test_lock_rwlock(PyObject *self, PyObject *obj) { struct test_rwlock_data test_data = {.nthreads = 3}; + _PyRWMutex_Lock(&test_data.rw); + assert(test_data.rw.bits == 1); + + _PyRWMutex_Unlock(&test_data.rw); + assert(test_data.rw.bits == 0); + // Start two readers PyThread_start_new_thread(rdlock_thread, &test_data); PyThread_start_new_thread(rdlock_thread, &test_data); diff --git a/Python/lock.c b/Python/lock.c index 1883aff7b5e03c..35eb6cb2b6ef0b 100644 --- a/Python/lock.c +++ b/Python/lock.c @@ -354,16 +354,54 @@ _PyOnceFlag_CallOnceSlow(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg) } } +#define _Py_WRITE_LOCKED 1 #define _PyRWMutex_READER_SHIFT 2 +#define _Py_RWMUTEX_MAX_READERS (UINTPTR_MAX >> _PyRWMutex_READER_SHIFT) + +static uintptr_t +rwmutex_set_parked_and_wait(_PyRWMutex *rwmutex, uintptr_t bits) +{ + // Set _Py_HAS_PARKED and wait until we are woken up. + if ((bits & _Py_HAS_PARKED) == 0) { + uintptr_t newval = bits | _Py_HAS_PARKED; + if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, + &bits, newval)) { + return bits; + } + bits = newval; + } + + _PyParkingLot_Park(&rwmutex->bits, &bits, sizeof(bits), -1, NULL, 1); + return _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); +} + +// The number of readers holding the lock +static uintptr_t +rwmutex_reader_count(uintptr_t bits) +{ + return bits >> _PyRWMutex_READER_SHIFT; +} void _PyRWMutex_RLock(_PyRWMutex *rwmutex) { uintptr_t bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); for (;;) { - // If the lock is not write-locked and there is no writer waiting, then - // we can increment the reader count. - if ((bits & (_Py_LOCKED|_Py_HAS_PARKED)) == 0) { + if ((bits & _Py_WRITE_LOCKED)) { + // The lock is write-locked. + bits = rwmutex_set_parked_and_wait(rwmutex, bits); + continue; + } + else if ((bits & _Py_HAS_PARKED)) { + // There is at least one waiting writer. We can't grab the lock + // because we don't want to starve the writer. Instead, we park + // ourselves and wait for the writer to eventually wake us up. + bits = rwmutex_set_parked_and_wait(rwmutex, bits); + continue; + } + else { + // The lock is unlocked or read-locked. Try to grab it. + assert(rwmutex_reader_count(bits) < _Py_RWMUTEX_MAX_READERS); uintptr_t newval = bits + (1 << _PyRWMutex_READER_SHIFT); if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, &bits, newval)) { @@ -371,19 +409,6 @@ _PyRWMutex_RLock(_PyRWMutex *rwmutex) } return; } - - // Set _Py_HAS_PARKED if it's not already set. - if ((bits & _Py_HAS_PARKED) == 0) { - uintptr_t newval = bits | _Py_HAS_PARKED; - if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, - &bits, newval)) { - continue; - } - bits = newval; - } - - _PyParkingLot_Park(&rwmutex->bits, &bits, sizeof(bits), -1, NULL, 1); - bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); } } @@ -393,7 +418,7 @@ _PyRWMutex_RUnlock(_PyRWMutex *rwmutex) uintptr_t bits = _Py_atomic_add_uintptr(&rwmutex->bits, -(1 << _PyRWMutex_READER_SHIFT)); bits -= (1 << _PyRWMutex_READER_SHIFT); - if ((bits >> _PyRWMutex_READER_SHIFT) == 0 && (bits & _Py_HAS_PARKED)) { + if (rwmutex_reader_count(bits) == 0 && (bits & _Py_HAS_PARKED)) { _PyParkingLot_UnparkAll(&rwmutex->bits); return; } @@ -409,23 +434,14 @@ _PyRWMutex_Lock(_PyRWMutex *rwmutex) if ((bits & ~_Py_HAS_PARKED) == 0) { if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, &bits, - bits | _Py_LOCKED)) { - return; - } - continue; - } - - if (!(bits & _Py_HAS_PARKED)) { - if (!_Py_atomic_compare_exchange_uintptr(&rwmutex->bits, - &bits, - bits | _Py_HAS_PARKED)) { + bits | _Py_WRITE_LOCKED)) { continue; } - bits |= _Py_HAS_PARKED; + return; } - _PyParkingLot_Park(&rwmutex->bits, &bits, sizeof(bits), -1, NULL, 1); - bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); + // Otherwise, we have to wait. + bits = rwmutex_set_parked_and_wait(rwmutex, bits); } } @@ -433,7 +449,9 @@ void _PyRWMutex_Unlock(_PyRWMutex *rwmutex) { uintptr_t old_bits = _Py_atomic_exchange_uintptr(&rwmutex->bits, 0); - assert(old_bits >> _PyRWMutex_READER_SHIFT == 0); + + assert((old_bits & _Py_WRITE_LOCKED) && "lock was not write-locked"); + assert(rwmutex_reader_count(old_bits) == 0 && "lock was read-locked"); if ((old_bits & _Py_HAS_PARKED) != 0) { _PyParkingLot_UnparkAll(&rwmutex->bits); From e8419d783ca38f7542c8c627b88ea841e557291f Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 14 Dec 2023 12:50:15 -0500 Subject: [PATCH 3/6] Update comments and add assertions. Also describe _PyRWMutex using bit patterns instead of ASCII art box. This matches how PyMutex is described. --- Include/internal/pycore_lock.h | 33 ++++++++++++++++++--------------- Python/lock.c | 11 +++++++---- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h index baaf78a9f18638..d873775c18e2cf 100644 --- a/Include/internal/pycore_lock.h +++ b/Include/internal/pycore_lock.h @@ -214,23 +214,26 @@ _PyOnceFlag_CallOnce(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg) } // A readers-writer (RW) lock. The lock supports multiple concurrent readers or -// a single writer. The lock is write-preferring: if a writer is waiting, then -// new readers will be blocked. This avoids starvation of writers. +// a single writer. The lock is write-preferring: if a writer is waiting while +// the lock is read-locked then, new readers will be blocked. This avoids +// starvation of writers. // -// The bitfield is structured as follows: +// In C++, the equivalent synchronization primitive is std::shared_mutex +// with shared ("read") and exclusive ("write") locking. // -// N ... 2 1 0 -// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -// | reader_count |P |WL| -// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// The two least significant bits are used to indicate if the lock is +// write-locked and if there are parked threads (either readers or writers) +// waiting to acquire the lock. The remaining bits are used to indicate the +// number of readers holding the lock. // -// The least significant bit (WL / _Py_WRITE_LOCKED) indicates if the mutex is -// write-locked. The next bit (PK/ _Py_HAS_PARKED) indicates if there are -// parked threads (either readers or writers). The remaining bits -// (reader_count) indicate the number of readers holding the lock. +// 0b000..00000: unlocked +// 0bnnn..nnn00: nnn..nnn readers holding the lock +// 0bnnn..nnn10: nnn..nnn readers holding the lock and a writer is waiting +// 0b00000..001: write-locked +// 0b00000..011: write-locked and readers or other writers are waiting // -// Note that reader_count must be zero if WL is set, and vice versa. The lock -// can only be held by readers or a writer, but not both. +// Note that reader_count must be zero if the lock is held by a writer, and +// vice versa. The lock can only be held by readers or a writer, but not both. // // The design is optimized for simplicity of the implementation. The lock is // not fair: if fairness is desired, use an additional PyMutex to serialize @@ -239,11 +242,11 @@ typedef struct { uintptr_t bits; } _PyRWMutex; -// Read lock +// Read lock (i.e., shared lock) PyAPI_FUNC(void) _PyRWMutex_RLock(_PyRWMutex *rwmutex); PyAPI_FUNC(void) _PyRWMutex_RUnlock(_PyRWMutex *rwmutex); -// Write lock +// Write lock (i.e., exclusive lock) PyAPI_FUNC(void) _PyRWMutex_Lock(_PyRWMutex *rwmutex); PyAPI_FUNC(void) _PyRWMutex_Unlock(_PyRWMutex *rwmutex); diff --git a/Python/lock.c b/Python/lock.c index 35eb6cb2b6ef0b..4836f8466df4ed 100644 --- a/Python/lock.c +++ b/Python/lock.c @@ -388,14 +388,16 @@ _PyRWMutex_RLock(_PyRWMutex *rwmutex) uintptr_t bits = _Py_atomic_load_uintptr_relaxed(&rwmutex->bits); for (;;) { if ((bits & _Py_WRITE_LOCKED)) { - // The lock is write-locked. + // A writer already holds the lock. bits = rwmutex_set_parked_and_wait(rwmutex, bits); continue; } else if ((bits & _Py_HAS_PARKED)) { - // There is at least one waiting writer. We can't grab the lock - // because we don't want to starve the writer. Instead, we park - // ourselves and wait for the writer to eventually wake us up. + // Reader(s) hold the lock, but there is at least one waiting + // writer. We can't grab the lock because we don't want to starve + // the writer. Instead, we park ourselves and wait for the writer + // to eventually wake us up. + assert(rwmutex_reader_count(bits) > 0); bits = rwmutex_set_parked_and_wait(rwmutex, bits); continue; } @@ -416,6 +418,7 @@ void _PyRWMutex_RUnlock(_PyRWMutex *rwmutex) { uintptr_t bits = _Py_atomic_add_uintptr(&rwmutex->bits, -(1 << _PyRWMutex_READER_SHIFT)); + assert(rw_mutex_reader_count(bits) > 0 && "lock was not read-locked"); bits -= (1 << _PyRWMutex_READER_SHIFT); if (rwmutex_reader_count(bits) == 0 && (bits & _Py_HAS_PARKED)) { From 1658506c336e45902488d123c3655fb3069b6234 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 14 Dec 2023 18:00:10 -0500 Subject: [PATCH 4/6] Fix typo --- Python/lock.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/lock.c b/Python/lock.c index 4836f8466df4ed..cb9ba41c7b5746 100644 --- a/Python/lock.c +++ b/Python/lock.c @@ -418,7 +418,7 @@ void _PyRWMutex_RUnlock(_PyRWMutex *rwmutex) { uintptr_t bits = _Py_atomic_add_uintptr(&rwmutex->bits, -(1 << _PyRWMutex_READER_SHIFT)); - assert(rw_mutex_reader_count(bits) > 0 && "lock was not read-locked"); + assert(rwmutex_reader_count(bits) > 0 && "lock was not read-locked"); bits -= (1 << _PyRWMutex_READER_SHIFT); if (rwmutex_reader_count(bits) == 0 && (bits & _Py_HAS_PARKED)) { From 95f27c35fa7468a9c0d2fb73c2dda25aed6e8bce Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 15 Dec 2023 11:41:02 -0500 Subject: [PATCH 5/6] Remove failing assertion --- Python/lock.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Python/lock.c b/Python/lock.c index cb9ba41c7b5746..f0ff1176941da8 100644 --- a/Python/lock.c +++ b/Python/lock.c @@ -393,11 +393,10 @@ _PyRWMutex_RLock(_PyRWMutex *rwmutex) continue; } else if ((bits & _Py_HAS_PARKED)) { - // Reader(s) hold the lock, but there is at least one waiting - // writer. We can't grab the lock because we don't want to starve - // the writer. Instead, we park ourselves and wait for the writer - // to eventually wake us up. - assert(rwmutex_reader_count(bits) > 0); + // Reader(s) hold the lock (or just gave up the lock), but there is + // at least one waiting writer. We can't grab the lock because we + // don't want to starve the writer. Instead, we park ourselves and + // wait for the writer to eventually wake us up. bits = rwmutex_set_parked_and_wait(rwmutex, bits); continue; } From 86fb6251dee1901c2b7535cd993ced3513e6c708 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 15 Dec 2023 11:43:23 -0500 Subject: [PATCH 6/6] Add comment describing state with _Py_HAS_PARKED set --- Include/internal/pycore_lock.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h index d873775c18e2cf..18a8896d97a548 100644 --- a/Include/internal/pycore_lock.h +++ b/Include/internal/pycore_lock.h @@ -229,6 +229,7 @@ _PyOnceFlag_CallOnce(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg) // 0b000..00000: unlocked // 0bnnn..nnn00: nnn..nnn readers holding the lock // 0bnnn..nnn10: nnn..nnn readers holding the lock and a writer is waiting +// 0b00000..010: unlocked with awoken writer about to acquire lock // 0b00000..001: write-locked // 0b00000..011: write-locked and readers or other writers are waiting //