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
We are going to need a variety of techniques to make dictionaries thread-safe in
--disable-gilbuilds. I expect this to be implemented across multiple PRs.For context, here is the change from the
nogil-3.12fork, but things might be done a bit differently in CPython 3.13: colesbury/nogil-3.12@d896dfc8dbPyDictKeysObjectneeds special handling (see below)PyDictOrValuesneeds special handling (see below)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
#ifdefguards.PyDictKeysObjectNote that
PyDictKeysObjectis NOT aPyObjectsubclass.We need a mutex in
PyDictKeysObjectbecause multiple threads may concurrently insert keys into sharedPyDictKeysObject. The mutex is only used for modifications to "shared" keys; non-shared keys rely on the locking for the dict object.In
--disable-gilbuilds, we want to avoid refcounting sharedPyDictKeysObjectfor 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-gilbuilds to make maintenance easier if there is not a negative perf impact.PyDictOrValuesPyDictOrValuesis the "managed" dict in some PyObject pre-headers. We need some locking/atomic operations to handle the transition betweenPyDictValues*and storing aPyDictObject*Optimistically Avoid Locking
See https://peps.python.org/pep-0703/#optimistically-avoiding-locking.
Linked PRs
dictoperations thread-safe without GIL #112247