Description
We are going to need a variety of techniques to make dictionaries thread-safe in --disable-gil
builds. I expect this to be implemented across multiple PRs.
For context, here is the change from the nogil-3.12
fork, but things might be done a bit differently in CPython 3.13: colesbury/nogil-3.12@d896dfc8db
- Most operations should acquire the dictionary object lock using the critical section API.
- Dictionary global version counter should use atomic increments
PyDictKeysObject
needs special handling (see below)PyDictOrValues
needs special handling (see below)- Accessing a single element should optimistically avoid locking for performance
In general, we want to avoid making changes which hurt the performance of the default build. The "critical sections" are no-ops in the default build, but we will need to take care with other changes. Some may be behind #ifdef
guards.
PyDictKeysObject
Note that PyDictKeysObject
is NOT a PyObject
subclass.
We need a mutex in PyDictKeysObject
because multiple threads may concurrently insert keys into shared PyDictKeysObject
. The mutex is only used for modifications to "shared" keys; non-shared keys rely on the locking for the dict object.
In --disable-gil
builds, we want to avoid refcounting shared PyDictKeysObject
for performance and thread-safety reasons. Instead shared keys objects should be freed during cyclic GC. (Non-shared keys don't need reference counting because they "owned" by a single dict object.) We may want to consider making this change for both the default build and --disable-gil
builds to make maintenance easier if there is not a negative perf impact.
PyDictOrValues
PyDictOrValues
is the "managed" dict in some PyObject pre-headers. We need some locking/atomic operations to handle the transition between PyDictValues*
and storing a PyDictObject*
Optimistically Avoid Locking
See https://peps.python.org/pep-0703/#optimistically-avoiding-locking.
Linked PRs
- gh-112075: Make some
dict
operations thread-safe without GIL #112247 - gh-112075: Adapt more dict methods to Argument Clinic #114256
- gh-112075: Add critical sections for most dict APIs #114508
- gh-112075: Add try-incref functions from nogil branch for use in dict thread safety #114512
- gh-112075: Use PyMem_* for allocating dict keys objects #114543
- gh-112075: Dictionary global version counter should use atomic increments #114568
- gh-112075: refactor dictionary lookup functions for better re-usability #114629
- gh-112075: Make PyDictKeysObject thread-safe #114741
- gh-112075: Make instance attributes stored in inline "dict" thread safe #114742
- gh-112075: Add a little specialization thread safety #114768
- gh-112075: Free-threaded dict odict basics #114778
- gh-112075: Add gc shared bits #114931
- gh-112075: Iterating a dict shouldn't require locks #115108
- gh-112075: Accessing a single element should optimistically avoid locking #115109
- gh-112075: Use relaxed stores for places where we may race with when reading lock-free #115786
- gh-112075: Remove compiler warning from apple clang #115855
- gh-112075: Avoid locking shared keys on every assignment #116087
- gh-112075: Enable freeing with qsbr and fallback to lock on key changed #116336
- gh-112075: Support freeing object memory via QSBR #116344
- gh-112075: Use atomic exchange in Py_SETREF so updating dict pointers doesn't race #116620
- gh-112075: _Py_dict_lookup needs to lock shared keys #117528
- gh-112075: Make _PyDict_LoadGlobal thread safe #117529
- gh-112075: Lock when inserting into shared keys #117824
- gh-112075: Fix race in constructing dict for instance #118499
- gh-112075: use per-thread dict version pool #118676
- gh-112075: Fix dict thread safety issues #119288
- [3.13] gh-112075: Fix dict thread safety issues (GH-119288) #121545
- gh-112075: Remove critical section in dict.get #129336