GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Make `dict` objects thread-safe in `--disable-gil` builds · Issue #112075 · python/cpython

Make dict objects thread-safe in --disable-gil builds #112075

New issue
Copy link
New issue
Copy link
Closed
Closed
Make dict objects thread-safe in --disable-gil builds#112075
Copy link
Assignees
DinoV
Labels
3.13only security fixesonly security fixestopic-free-threadingtype-featureA feature request or enhancementA feature request or enhancement

Description

@colesbury
colesbury
opened on Nov 14, 2023
Issue body actions

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

  1. Most operations should acquire the dictionary object lock using the critical section API.
  2. Dictionary global version counter should use atomic increments
  3. PyDictKeysObject needs special handling (see below)
  4. PyDictOrValues needs special handling (see below)
  5. 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
  • gh-112075: Remove _PyObject_SetManagedDict() function #139737

Activity

Next
  1. colesbury
    added
    type-featureA feature request or enhancementA feature request or enhancement
    3.13only security fixesonly security fixes
    topic-free-threading
    on Nov 14, 2023
  2. chgnrdv commented on Nov 17, 2023

    @chgnrdv
    chgnrdv
    on Nov 17, 2023
    Contributor
    More actions

    I'd like to work on this one.

  3. colesbury commented on Nov 17, 2023

    @colesbury
    colesbury
    on Nov 17, 2023
    ContributorAuthor
    More actions

    Thanks @chgnrdv! I think "making operations use the critical section API" can be started now, but most of the other steps still have some pre-requisites that are not yet implemented.

    The dict changes are going to be pretty big. It may make sense to convert a few operations at a time to use the critical section API.

  4. chgnrdv commented on Nov 18, 2023

    @chgnrdv
    chgnrdv
    on Nov 18, 2023
    Contributor
    More actions

    @colesbury , I made the PR #112247 that covers the most obvious cases. Lots of methods are still to be converted, mostly the ones that access a single element and are expected to optimistically avoid locking. Most of them do not access PyDictObject fields directly and use _Py_dict_lookup function to get dict values.

  5. DinoV
    self-assigned this
    on Jan 17, 2024
  6. DinoV commented on Jan 23, 2024

    @DinoV
    DinoV
    on Jan 23, 2024
    Contributor
    More actions

    Regarding:

    In --disable-gil builds, we want to avoid refcounting shared PyDictKeysObject for performance and thread-safety reasons.

    This may be a non-issue in 3.12 and beyond. With the introduction of managed dicts when the values are all stored in the in-line array the shared dict keys is not incremented. If we make a full dict for the object then we will end up initializing it and adding a reference, but it seems like at least as far as the Python test suite is concerned that's a rare event. It seems like most processes hit it less than 10 times, and a handful hit it around 100. test_regrtest seems to hit it around 300.

  7. colesbury commented on Jan 23, 2024

    @colesbury
    colesbury
    on Jan 23, 2024
    ContributorAuthor
    More actions

    I think this avoiding refcounting PyDictKeysObject is something we will want to do, but it doesn't have to happen immediately. We can start with atomic reference count operations in free-threaded builds for now.

    The prevalence of __dict__ attribute accesses varies greatly between projects. For example, PyTorch accesses self.__dict__ in every instantiation of nn.Module(), one of the basic types. To be clear, the issue is not really the overhead of atomic reference counting by themselves, but that it introduces a point of contention (inhibiting scaling) on state that's shared between all instances of a type. In other words, if you access self.__dict__ in a constructor, now creating instances of that type doesn't scale across threads. That's both something we want to avoid and surprising to users, because the shared state is an internal detail completely hidden from them.

    Reacted by Dino Viehland
  8. DinoV commented on Jan 23, 2024

    @DinoV
    DinoV
    on Jan 23, 2024
    Contributor
    More actions

    Can you explain more what you were thinking about freeing them during cyclic GC then? Given that Python's GC drives down reference counts I'm not 100% sure how that'd work. The only thing I can imagine working is that we maintain a separate list of all shared keys and note which ones weren't visited during a full GC, although maybe you had something better in mind :)

  9. colesbury commented on Jan 23, 2024

    @colesbury
    colesbury
    on Jan 23, 2024
    ContributorAuthor
    More actions

    The only thing I can imagine working is that we maintain a separate list of all shared keys and note which ones weren't visited during a full GC.

    That's the basic idea, but the list only needs to bother with shared keys for types that were deallocated. If the type is still alive, then the shared keys must be alive too.

    The strategy in nogil-3.12 was:

    1. Shared keys objects are eligible for collection only after the containing type is freed. When that happens, they get enqueued in a per-interpreter linked list of maybe-dead shared keys (called tracked_shared_keys):
      https://github.com/colesbury/nogil-3.12/blob/nogil-3.12/Include/internal/pycore_dict_state.h#L48

    2. To support this, shared keys objects have some extra state. They are actually PyDictSharedKeysObject, which embed a PyDictKeysObject.

    3. During GC, we mark any shared keys referenced by a dict as live.

    4. Later in the GC, we loop over the maybe-dead list (tracked_shared_keys), and free any shared keys that were not marked as live.

    The collection is simpler than general PyObject collection because keys objects are private data structures that can only be referenced by dicts.

    Reacted by Dino Viehland
  10. DinoV
    added 4 commits that reference this issue on Jan 23, 2024

    gh-112075: Adapt more dict methods to Argument Clinic (#114256)

    afe8f37

    gh-112075: Add try-incref functions from nogil branch for use in dict…

    4850410

    gh-112075: Use PyMem_* for allocating dict keys objects (#114543)

    3d71665

    gh-112075: Dictionary global version counter should use atomic increm…

    0cd9bac
  11. 54 remaining items

  12. DinoV
    closed this as completedon May 7, 2024
  13. SonicField
    added 2 commits that reference this issue on May 8, 2024

    pythongh-112075: Fix race in constructing dict for instance (python#1…

    4315b41

    pythongh-112075: use per-thread dict version pool (python#118676)

    a540070
  14. DinoV
    added a commit that references this issue on May 24, 2024

    gh-112075: Fix dict thread safety issues (#119288)

    f0ed186
  15. miss-islington
    added a commit that references this issue on Jul 9, 2024

    pythongh-112075: Fix dict thread safety issues (pythonGH-119288)

    89820ba
  16. colesbury
    added a commit that references this issue on Jul 9, 2024

    [3.13] gh-112075: Fix dict thread safety issues (GH-119288) (#121545)

    2f15383
  17. estyxx
    added a commit that references this issue on Jul 17, 2024

    pythongh-112075: Fix dict thread safety issues (python#119288)

    f811544
  18. Glyphack
    added 2 commits that reference this issue on Sep 2, 2024

    pythongh-112075: Adapt more dict methods to Argument Clinic (python#1…

    d078020

    pythongh-112075: Add try-incref functions from nogil branch for use i…

    f35e2a9
  19. LukasWoodtli
    added 3 commits that reference this issue on Jan 22, 2025

    pythongh-112075: Make PyDictKeysObject thread-safe (python#114741)

    eed7e93

    pythongh-112075: Accessing a single element should optimistically avo…

    de24ce3

    pythongh-112075: Iterating a dict shouldn't require locks (python#115108

    9b49cc5
  20. colesbury
    added a commit that references this issue on Jan 28, 2025

    gh-112075: Remove critical section in dict.get (gh-129336)

    64c417d
  21. vstinner
    added 2 commits that reference this issue on Oct 7, 2025

    pythongh-112075: Remove _PyObject_SetManagedDict() function

    24cbdcf

    gh-112075: Remove _PyObject_SetManagedDict() function (#139737)

    5f91d5d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @DinoV
    DinoV

Labels

3.13only security fixesonly security fixestopic-free-threadingtype-featureA feature request or enhancementA feature request or enhancement

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.