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 — Non-blocking `_PyMutex_LockTimed` spins and may fail unnecessarily in no-GIL builds · Issue #135871 · python/cpython

Non-blocking _PyMutex_LockTimed spins and may fail unnecessarily in no-GIL builds #135871

New issue
Copy link
New issue
Copy link
Closed
Closed
Non-blocking _PyMutex_LockTimed spins and may fail unnecessarily in no-GIL builds#135871
Copy link
Labels
3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixes3.15bugs and security fixesbugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@jtibbertsma
jtibbertsma
opened on Jun 24, 2025
Issue body actions

Bug report

Bug description:

Non-blocking _PyMutex_LockTimed spins and may fail unnecessarily (no-GIL build)

Branch: main (commit 2793b68f758c10fb63b264787f10d46a71fc8086)
Build: configured with --disable-gil (MAX_SPIN_COUNT > 0)
OS: all


Summary

_PyMutex_LockTimed() is supposed to return immediately when called with
timeout == 0 (non-blocking). In a no-GIL build it:

  1. Spins for up to MAX_SPIN_COUNT yields before returning;
  2. Can still return PY_LOCK_FAILURE even though the lock was released during that spin.

Timed/blocking calls (timeout > 0) also waste CPU because the spin loop
never reloads the lock word, so they can’t notice an unlock until after the
maximum spin count.

The bug is invisible in GIL builds because MAX_SPIN_COUNT is 0.


Root cause

/* Python/lock.c — excerpts */

if ((v & _Py_LOCKED) == 0) {
    if (_Py_atomic_compare_exchange_uint8(&m->_bits, &v, v | _Py_LOCKED))
        return PY_LOCK_ACQUIRED;
}
else if (timeout == 0) {            // executes only if FIRST load saw LOCKED
    return PY_LOCK_FAILURE;         // non-blocking, OK
}

/* … later … */
if (!(v & _Py_HAS_PARKED) && spin_count < MAX_SPIN_COUNT) {
    _Py_yield();
    spin_count++;
    continue;                       // BUG: never refreshes v
}
  • If our fast CAS loses a race, the else if guard is skipped, so a
    non-blocking call drops into the spin loop.
  • Inside that loop v is never reloaded, so the thread can’t see that the
    lock became free. After MAX_SPIN_COUNT iterations it falls through to
    the same timeout == 0 guard and fails spuriously.

Proposed fix

/* 1 — reload v each spin */
if (!(v & _Py_HAS_PARKED) && spin_count < MAX_SPIN_COUNT) {
    _Py_yield();
    spin_count++;
    v = _Py_atomic_load_uint8_relaxed(&m->_bits);   // ← added
    continue;
}

/* 2 — early-out for timeout == 0 */
if ((v & _Py_LOCKED) == 0) {
    if (_Py_atomic_compare_exchange_uint8(&m->_bits, &v, v | _Py_LOCKED))
        return PY_LOCK_ACQUIRED;
}
if (timeout == 0) {                               // ← moved outside else
    return PY_LOCK_FAILURE;
}

Result

  • Non-blocking calls now return immediately: success if the CAS wins,
    failure if it loses – no spinning, no parking.
  • Timed/blocking calls still spin for fairness, but they now reload the
    lock word each iteration, so they acquire promptly once the lock is free.

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

  • gh-135871: Fix needless spinning in _PyMutex_LockTimed (timeout==0) #135872
  • [3.14] gh-135871: Fix needless spinning in _PyMutex_LockTimed with zero timeout (gh-135872) #135946
  • [3.13] gh-135871: Fix needless spinning in _PyMutex_LockTimed with zero timeout (gh-135872) #135947
  • gh-135871: Reload lock internal state while spinning in PyMutex_LockTimed #146064

Activity

  1. jtibbertsma
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Jun 24, 2025
  2. jtibbertsma
    added 4 commits that reference this issue on Jun 24, 2025

    pythongh-135871: Fix needless spinning in _PyMutex_LockTimed (timeout…

    94f04f5

    pythongh-135871: Fix needless spinning in _PyMutex_LockTimed (timeout…

    88e8a1c

    pythongh-135871: Fix needless spinning in _PyMutex_LockTimed (timeout…

    45811fd

    pythongh-135871: Fix needless spinning in _PyMutex_LockTimed (timeout…

    7296a7d
  3. picnixz
    added
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    topic-free-threading
    on Jun 24, 2025
  4. picnixz commented on Jun 24, 2025

    @picnixz
    picnixz
    on Jun 24, 2025
    Member
    More actions

    cc @ZeroIntensity

  5. ZeroIntensity
    added
    3.13only security fixesonly security fixes
    3.14bugs and security fixesbugs and security fixes
    3.15bugs and security fixesbugs and security fixes
    on Jun 24, 2025
  6. ZeroIntensity commented on Jun 24, 2025

    @ZeroIntensity
    ZeroIntensity
    on Jun 24, 2025
    Member
    More actions

    The issue and proposed fix make sense to me, but I'm also a little suspicious. I would think that this continue needs to reload the lock bits, because otherwise we're going to spin without reloading, as stated in the issue. But maybe this was omitted for contention reasons? I can't find anything in the PR about it.

    Sam based PyMutex off of WebKit's locking implementation. Reading the article, it seems that they do include an atomic load for each spin, but it was also written several years before PyMutex went in. There's a chance they removed that atomic for performance's sake in-between 2016 and 2023. I'll wait for his input.

    Reacted by Duprat
  7. vstinner commented on Jun 24, 2025

    @vstinner
    vstinner
    on Jun 24, 2025
    Member
    More actions

    cc @colesbury @mpage

  8. colesbury
    added 2 commits that reference this issue on Jun 25, 2025

    [3.14] gh-135871: Fix needless spinning in `_PyMutex_LockTimed` with …

    80fc62f

    [3.13] gh-135871: Fix needless spinning in `_PyMutex_LockTimed` with …

    c64df2a
  9. ZeroIntensity commented on Jun 30, 2025

    @ZeroIntensity
    ZeroIntensity
    on Jun 30, 2025
    Member
    More actions

    I tracked down WebKit's locking implementation, and it seems they do have the atomic load for each iteration: https://github.com/WebKit/WebKit/blob/7ebf83c48305f8aac9f37af71af77dd66b758811/Source/WTF/wtf/LockAlgorithmInlines.h#L48

    We found that it hurts performance for us, though. Is everyone good with closing this?

  10. colesbury commented on Jun 30, 2025

    @colesbury
    colesbury
    on Jun 30, 2025
    Contributor
    More actions

    No, we should fix it, but carefully in a way that doesn't hurt performance.

  11. ZeroIntensity commented on Jun 30, 2025

    @ZeroIntensity
    ZeroIntensity
    on Jun 30, 2025
    Member
    More actions

    Ok. Perhaps only reloading every few spins or so?

  12. dpdani commented on Feb 27, 2026

    @dpdani
    dpdani
    on Feb 27, 2026
    Contributor
    More actions

    I'm interested into taking this issue if no one objects, and I've started to look into it.

  13. vstinner commented on Mar 2, 2026

    @vstinner
    vstinner
    on Mar 2, 2026
    Member
    More actions

    Go ahead @dpdani!

    Reacted by Daniele Parmeggiani
  14. dpdani
    added a commit that references this issue on Mar 17, 2026

    pythongh-135871: Reload lock internal state while spinning in `_PyMut…

    7217ad0
  15. colesbury
    added a commit that references this issue on Mar 23, 2026

    gh-135871: Reload lock internal state while spinning in `PyMutex_Lock…

    daa159f
  16. CuriousLearner
    added a commit that references this issue on Mar 24, 2026

    Merge branch 'fix-pythongh-138577' of github.com:CuriousLearner/cpyth…

    78b2c6a
  17. kumaraditya303 commented on Apr 5, 2026

    @kumaraditya303
    kumaraditya303
    on Apr 5, 2026
    Contributor
    More actions

    Fixed by #146064

  18. kumaraditya303
    closed this as completedon Apr 5, 2026
  19. clin1234
    added a commit that references this issue on Apr 16, 2026

    pythongh-135871: Reload lock internal state while spinning in `PyMute…

    28b767f
  20. ljfp
    added a commit that references this issue on Apr 25, 2026

    pythongh-135871: Reload lock internal state while spinning in `PyMute…

    29cb7de
  21. corona10
    added a commit that references this issue on Sep 27, 2026

    gh-157914: Revert "gh-135871: Reload lock internal state while spinni…

    baec764
  22. hugovk
    added a commit that references this issue on Sep 30, 2026

    [3.15] gh-157914: Revert "gh-135871: Reload lock internal state while…

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixes3.15bugs and security fixesbugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    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.