Skip to content

Commit 9783997

Browse files
committed
Move seqlock implementation to lock.c
1 parent 770a6e5 commit 9783997

File tree

4 files changed

+125
-52
lines changed

4 files changed

+125
-52
lines changed

Include/internal/pycore_lock.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,39 @@ PyAPI_FUNC(void) _PyRWMutex_RUnlock(_PyRWMutex *rwmutex);
251251
PyAPI_FUNC(void) _PyRWMutex_Lock(_PyRWMutex *rwmutex);
252252
PyAPI_FUNC(void) _PyRWMutex_Unlock(_PyRWMutex *rwmutex);
253253

254-
void _Py_yield(void);
254+
// Similar to linux seqlock: https://en.wikipedia.org/wiki/Seqlock
255+
// We use a sequence number to lock the writer, an even sequence means we're unlocked, an odd
256+
// sequence means we're locked. Readers will read the sequence before attempting to read the
257+
// underlying data and then read the sequence number again after reading the data. If the
258+
// sequence has not changed the data is valid.
259+
//
260+
// Differs a little bit in that we use CAS on sequence as the lock, instead of a seperate spin lock.
261+
// The writer can also detect that the undelering data has not changed and abandon the write
262+
// and restore the previous sequence.
263+
typedef struct {
264+
int sequence;
265+
} _PySeqLock;
266+
267+
// Lock the sequence lock for the writer
268+
PyAPI_FUNC(void) _PySeqLock_LockWrite(_PySeqLock *seqlock);
269+
270+
// Unlock the sequence lock and move to the next sequence number.
271+
PyAPI_FUNC(void) _PySeqLock_UnlockWrite(_PySeqLock *seqlock);
272+
273+
// Abandon the current update indicating that no mutations have occured
274+
// and restore the previous sequence value.
275+
PyAPI_FUNC(void) _PySeqLock_AbandonWrite(_PySeqLock *seqlock);
276+
277+
// Begin a read operation and return the current sequence number.
278+
PyAPI_FUNC(int) _PySeqLock_BeginRead(_PySeqLock *seqlock);
279+
280+
// End the read operation and confirm that the sequence number has not changed.
281+
// Returns 1 if the read was successful or 0 if the read should be re-tried.
282+
PyAPI_FUNC(int) _PySeqLock_EndRead(_PySeqLock *seqlock, int previous);
283+
284+
// Check if the lock was held during a fork and clear the lock. Returns 1
285+
// if the lock was held and any associated datat should be cleared.
286+
PyAPI_FUNC(int) _PySeqLock_AfterFork(_PySeqLock *seqlock);
255287

256288
#ifdef __cplusplus
257289
}

Include/internal/pycore_typeobject.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct _types_runtime_state {
3131
struct type_cache_entry {
3232
unsigned int version; // initialized from type->tp_version_tag
3333
#ifdef Py_GIL_DISABLED
34-
int sequence;
34+
_PySeqLock sequence;
3535
#endif
3636
PyObject *name; // reference to exactly a str or None
3737
PyObject *value; // borrowed reference or NULL

Objects/typeobject.c

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "pycore_code.h" // CO_FAST_FREE
77
#include "pycore_dict.h" // _PyDict_KeysSize()
88
#include "pycore_frame.h" // _PyInterpreterFrame
9+
#include "pycore_lock.h" // _PySeqLock_*
910
#include "pycore_long.h" // _PyLong_IsNegative()
1011
#include "pycore_memoryobject.h" // _PyMemoryView_FromBufferProc()
1112
#include "pycore_modsupport.h" // _PyArg_NoKwnames()
@@ -4895,43 +4896,21 @@ static void
48954896
update_cache_gil_disabled(struct type_cache_entry *entry, PyObject *name,
48964897
unsigned int version_tag, PyObject *value)
48974898
{
4898-
// Similar to linux seqlock: https://en.wikipedia.org/wiki/Seqlock
4899-
// We use a sequence number to lock the writer, an even sequence means we're unlocked, an odd
4900-
// sequence means we're locked.
4901-
// Differs a little bit in that we use CAS on sequence as the lock, instead of a seperate spin lock.
4902-
// If our writer detects that another thread has already done the same write we'll just bail
4903-
// and restore the previous sequence number without doing any updates.
4904-
4905-
// lock the entry by setting by moving to an odd sequence number
4906-
int prev = entry->sequence;
4907-
while (1) {
4908-
if (TYPE_CACHE_IS_UPDATING(prev)) {
4909-
// Someone else is currently updating the cache
4910-
_Py_yield();
4911-
prev = _Py_atomic_load_int32_relaxed(&entry->sequence);
4912-
} else if(_Py_atomic_compare_exchange_int32(&entry->sequence, &prev, prev + 1)) {
4913-
// We've locked the cache
4914-
break;
4915-
} else {
4916-
_Py_yield();
4917-
}
4918-
}
4899+
_PySeqLock_LockWrite(&entry->sequence);
49194900

49204901
// update the entry
49214902
if (entry->name == name &&
49224903
entry->value == value &&
49234904
entry->version == version_tag) {
4924-
// We reaced with another update, bail and restore previous sequence.
4925-
_Py_atomic_exchange_int32(&entry->sequence, prev);
4905+
// We raced with another update, bail and restore previous sequence.
4906+
_PySeqLock_AbandonWrite(&entry->sequence);
49264907
return;
49274908
}
49284909

49294910
update_cache(entry, name, version_tag, value);
49304911

49314912
// Then update sequence to the next valid value
4932-
int new_sequence = prev + 2;
4933-
assert(!TYPE_CACHE_IS_UPDATING(new_sequence));
4934-
_Py_atomic_exchange_int32(&entry->sequence, new_sequence);
4913+
_PySeqLock_UnlockWrite(&entry->sequence);
49354914
}
49364915

49374916
#endif
@@ -4941,10 +4920,8 @@ void _PyTypes_AfterFork() {
49414920
struct type_cache *cache = get_type_cache();
49424921
for (int i = 0; i < 1 << MCACHE_SIZE_EXP; i++) {
49434922
struct type_cache_entry *entry = &cache->hashtable[i];
4944-
int sequence = _Py_atomic_load_int_acquire(&entry->sequence);
4945-
if (TYPE_CACHE_IS_UPDATING(sequence)) {
4923+
if (_PySeqLock_AfterFork(&entry->sequence)) {
49464924
// Entry was in the process of updating while forking, clear it...
4947-
entry->sequence = 0;
49484925
entry->value = NULL;
49494926
entry->name = NULL;
49504927
entry->version = 0;
@@ -4968,27 +4945,22 @@ _PyType_Lookup(PyTypeObject *type, PyObject *name)
49684945
#ifdef Py_GIL_DISABLED
49694946
// synchronize-with other writing threads by doing an acquire load on the sequence
49704947
while (1) {
4971-
int sequence = _Py_atomic_load_int_acquire(&entry->sequence);
4972-
if (!TYPE_CACHE_IS_UPDATING(sequence)) {
4973-
if (_Py_atomic_load_uint32_relaxed(&entry->version) == type->tp_version_tag &&
4974-
_Py_atomic_load_ptr_relaxed(&entry->name) == name) {
4975-
assert(_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
4976-
OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
4977-
OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
4978-
PyObject *value = _Py_atomic_load_ptr_relaxed(&entry->value);
4979-
4980-
// Synchronize again and validate that the entry hasn't been updated
4981-
// while we were readying the values.
4982-
if (_Py_atomic_load_int_acquire(&entry->sequence) == sequence) {
4983-
return value;
4984-
}
4985-
} else {
4986-
// Cache miss
4987-
break;
4988-
}
4948+
int sequence = _PySeqLock_BeginRead(&entry->sequence);
4949+
if (_Py_atomic_load_uint32_relaxed(&entry->version) == type->tp_version_tag &&
4950+
_Py_atomic_load_ptr_relaxed(&entry->name) == name) {
4951+
assert(_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
4952+
OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
4953+
OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
4954+
PyObject *value = _Py_atomic_load_ptr_relaxed(&entry->value);
4955+
4956+
// If the sequence is still valid then we're done
4957+
if (_PySeqLock_EndRead(&entry->sequence, sequence)) {
4958+
return value;
4959+
}
4960+
} else {
4961+
// cache miss
4962+
break;
49894963
}
4990-
// We are in progress of updating the cache, retry
4991-
_Py_yield();
49924964
}
49934965
#else
49944966
if (entry->version == type->tp_version_tag &&

Python/lock.c

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ struct mutex_entry {
3636
int handed_off;
3737
};
3838

39-
void
39+
static void
4040
_Py_yield(void)
4141
{
4242
#ifdef MS_WINDOWS
@@ -459,3 +459,72 @@ _PyRWMutex_Unlock(_PyRWMutex *rwmutex)
459459
_PyParkingLot_UnparkAll(&rwmutex->bits);
460460
}
461461
}
462+
463+
#define SEQLOCK_IS_UPDATING(sequence) (sequence & 0x01)
464+
465+
void _PySeqLock_LockWrite(_PySeqLock *seqlock)
466+
{
467+
// lock the entry by setting by moving to an odd sequence number
468+
int prev = seqlock->sequence;
469+
while (1) {
470+
if (SEQLOCK_IS_UPDATING(prev)) {
471+
// Someone else is currently updating the cache
472+
_Py_yield();
473+
prev = _Py_atomic_load_int32_relaxed(&seqlock->sequence);
474+
} else if (_Py_atomic_compare_exchange_int32(&seqlock->sequence, &prev, prev + 1)) {
475+
// We've locked the cache
476+
break;
477+
} else {
478+
_Py_yield();
479+
}
480+
}
481+
}
482+
483+
void _PySeqLock_AbandonWrite(_PySeqLock *seqlock)
484+
{
485+
int new_seq = seqlock->sequence - 1;
486+
assert(!SEQLOCK_IS_UPDATING(new_seq));
487+
_Py_atomic_exchange_int32(&seqlock->sequence, new_seq);
488+
}
489+
490+
void _PySeqLock_UnlockWrite(_PySeqLock *seqlock)
491+
{
492+
int new_seq = seqlock->sequence + 1;
493+
assert(!SEQLOCK_IS_UPDATING(new_seq));
494+
_Py_atomic_exchange_int32(&seqlock->sequence, new_seq);
495+
}
496+
497+
int _PySeqLock_BeginRead(_PySeqLock *seqlock)
498+
{
499+
int sequence = _Py_atomic_load_int_acquire(&seqlock->sequence);
500+
while (SEQLOCK_IS_UPDATING(sequence)) {
501+
_Py_yield();
502+
sequence = _Py_atomic_load_int_acquire(&seqlock->sequence);
503+
}
504+
505+
return sequence;
506+
}
507+
508+
int _PySeqLock_EndRead(_PySeqLock *seqlock, int previous)
509+
{
510+
// Synchronize again and validate that the entry hasn't been updated
511+
// while we were readying the values.
512+
if (_Py_atomic_load_int_acquire(&seqlock->sequence) == previous) {
513+
return 1;
514+
}
515+
516+
_Py_yield();
517+
return 0;
518+
}
519+
520+
int _PySeqLock_AfterFork(_PySeqLock *seqlock)
521+
{
522+
// Synchronize again and validate that the entry hasn't been updated
523+
// while we were readying the values.
524+
if (SEQLOCK_IS_UPDATING(seqlock->sequence)) {
525+
seqlock->sequence = 0;
526+
return 1;
527+
}
528+
529+
return 0;
530+
}

0 commit comments

Comments
 (0)