Free-threading guidance for CPython contributors¶
Practical guidance for contributors working on free-threading in CPython. It draws on recurring feedback in CPython code reviews and explains common mistakes and how to avoid them.
This guidance was written by a CPython core developer and reviewed by other core developers. It is not official CPython policy.
This document concerns CPython internals. While some advice might be relevant for third-party extension modules, it is suggested you first start with the free-threading extension guide.
The source for this site is on GitHub.
Contents¶
- Critical sections
- Whether a critical section is needed
- Critical section has the wrong extent
- Critical section locks the wrong object
- Critical section leaked on an error path
- Critical section suspension semantics
- Argument Clinic @critical_section
- Atomics and memory ordering
- Whether an access must be atomic
- One side of the access is atomic, the other is not
- Memory ordering is wrong
- Read-modify-write is not atomic
- Reference handling
- Borrowed reference is not safe here
- tp_iternext and shared iterators
- Deferred reference counting and VM stack references
- Lock choice and discipline
- Wrong synchronization mechanism
- Lock ordering and deadlock
- Lock lives at the wrong scope
- Stop-the-world
- Unsafe work while the world is stopped
- Stop-the-world where a lock would do
- Tests
- Needs a test
- The test cannot actually hit the race
- Test placement and gating
- ThreadSanitizer
- Performance
- Regression in the default build
- Scaling and contention
- Observable behaviour
- Define the thread-safety contract
- Why the code is already safe, with no mechanism named
- Dual-build portability
- Build guards and the FT_* wrappers
Critical sections¶
needed or not, wrong scope, wrong object, leaks, suspension
Whether a critical section is needed¶
A critical section protects an object's mutable state. Use one when another thread can access the same state at the same time and at least one thread can change it.
Protect reads as well as writes. Suppose code changes self->exports while
holding self's critical section. Every competing read of self->exports
must hold the same critical section:
Py_ssize_t exports;
Py_BEGIN_CRITICAL_SECTION(self);
exports = self->exports;
Py_END_CRITICAL_SECTION();
Locking only the code that changes the field is not enough. An unlocked read can overlap a write.
Some operations depend on more than one access. They may read a field, check its value, and then use another field based on that result. Keep these steps in the same critical section. Otherwise, another thread can change the object after the check but before the value is used. A call to a helper can also access the object indirectly. Check what called functions do before deciding that a method does not need locking.
A critical section is not needed when no competing thread can change the state. Common examples are a new object that has not yet been published, immutable state, thread-local state, and code whose caller already holds the required lock. A method also does not need its object lock when it does not access the object's state, either directly or through a called function. Do not add a critical section only because a function receives an object as an argument.
Identify which fields can change after publication. Work that depends only on arguments or immutable state can happen before acquiring the lock. Once code reads mutable protected state, keep the critical section until it no longer depends on that state.
A containing object's lock can protect a private contained object. This is safe only when every access to the contained object goes through the containing object and follows the same locking rule. Document this ownership rule. Use lock-held helpers and lock assertions to make the rule clear.
For an Argument Clinic entry point, use @critical_section when the whole
operation needs the object's lock. For other code, a small wrapper can enter
the critical section and call a lock-held implementation.
When code intentionally does not take a lock because of a less obvious
safety rule, document that rule and assert it where possible. For example, a
constructor-only helper can explain that the object has not been published
and, when applicable, assert _PyObject_IsUniquelyReferenced(obj).
Critical section has the wrong extent¶
Protect the complete operation on mutable state. First list the fields the operation reads or writes. Then identify which object's lock protects each field. Acquire that lock before the first relevant read, and keep dependent checks and uses in the same critical section.
Do not unlock after reading a pointer if another thread can invalidate it before the pointer is used. Obtain an owned reference or copy while locked, or keep the lock through the use. The same rule applies to a check followed by an update: another thread must not be able to change the checked state between those steps.
Keep unrelated work outside the section. Type checks, argument conversion, and result construction often do not need the object's lock. Where practical, use the protected part only to read or update the object's internal state.
When the lock is already held, prefer a suitable lock-held helper such as
_PyList_AppendTakeRef() instead of a public helper that acquires another
critical section. This avoids extra work and unnecessary nested critical
sections.
If a call that can suspend the critical section or invoke arbitrary code must occur inside it, assume the protected state may change. Do not carry borrowed pointers or checked conditions across the call without revalidating them afterward. See Critical section suspension semantics.
Do not split a dependent operation solely to impose a lock order. Python
critical sections avoid lock-order deadlocks by suspending an active outer
section when another acquisition blocks. This means another thread may
change the outer object while its section is suspended. If the operation
needs two objects to remain protected together, use
Py_BEGIN_CRITICAL_SECTION2() rather than two nested sections. See Critical
section suspension semantics.
Critical section locks the wrong object¶
Py_BEGIN_CRITICAL_SECTION(obj) locks only obj. It does not automatically
protect other objects that obj refers to. It also does not protect an
object that refers to obj.
Start by listing the mutable fields that the operation reads or changes.
For each field, identify which object's lock protects it. Then lock that
object. Do not assume that self is always the right object.
For example, a set iterator uses state from two objects. The set owns the table of entries. The iterator owns its current position and whether it is exhausted. Locking the set protects the table, but not fields stored in the iterator. Locking the iterator does not protect the set's table.
Container operations usually lock the container, not the items in it. Comparing an item with other elements does not by itself mean that the item must be locked. The container lock protects the structure of the heap, list, set, or table.
Sometimes an operation uses mutable state from two different objects and
needs both objects to remain consistent. Use
Py_BEGIN_CRITICAL_SECTION2 to lock both objects in that case. Do not lock a
second object only because the function receives it or mentions it. Lock it
only when the operation depends on mutable state that its lock protects.
Do not omit a required lock because of concerns about lock ordering. Critical sections are designed to avoid lock-ordering and re-entrancy deadlocks. Avoid unnecessary locks because they add work and protect no state that the operation needs.
Critical section leaked on an error path¶
Treat Py_BEGIN_CRITICAL_SECTION and Py_END_CRITICAL_SECTION as a single
lexical region. Every control-flow path after the begin must reach the
matching end. A return, error goto, or a jump beyond the end leaves
the object mutex held and can deadlock later users.
Perform type checks and other validation that can return before entering the section, rather than acquiring a lock for an object that may not have the required layout.
Keep error handling simple enough to audit this pairing. Prefer a
_lock_held helper that asserts its object is locked, plus a small
wrapper that opens and closes the section around the call. For Argument
Clinic methods, @critical_section can generate that wrapper.
Avoid using goto to manage a critical section: it makes skipped cleanup
easy to introduce and has been reported to cause MSVC compile problems. If
cleanup is needed, structure the paths so they converge on the explicit
Py_END_CRITICAL_SECTION.
Critical section suspension semantics¶
Code between Py_BEGIN_CRITICAL_SECTION and
Py_END_CRITICAL_SECTION is not guaranteed to hold the lock continuously.
A call can suspend the critical section. The lock is released and acquired
again before execution continues. The object may have changed while the lock
was released. Acquiring the lock again does not restore the old state.
A critical section can be suspended when the thread detaches, such as during a blocking operation. It can also happen when code enters another critical section and has to wait for that object's lock. Calls that run arbitrary Python code can re-enter the interpreter and modify objects that the caller was using.
Some re-entrant calls are easy to miss:
Py_DECREFcan run deallocation code when the reference count reaches zero. This can invoke weak-reference callbacks or a__del__method.- A comparison can call methods such as
__eq__or__lt__. - Hashing and conversions can call methods such as
__hash__,__index__, or__int__. - Descriptors, callbacks, and calls through type slots can also execute arbitrary Python code.
Treat each such call as a point where data you depend on can change under you. A borrowed pointer may no longer be valid. A saved list index may no longer be in range. A condition checked before the call may no longer be true. Two fields that agreed before the call may no longer agree afterward.
This problem also exists in a build with the GIL. It does not require two
threads. A __del__ or __eq__ method can run in the current thread and
modify an object before the C function resumes. The GIL does not protect C
code from changes made by re-entrant calls.
Do not carry a borrowed pointer or a checked condition across a possible re-entry point. Take an owned reference or a stable copy while the object is locked when that is sufficient. After the call, read the mutable state again and repeat any checks that later code depends on. Remember that an owned reference protects an object's lifetime, but not its mutable contents.
When the caller already holds the required lock, use a suitable _LockHeld
helper instead of an ordinary API that enters another critical section.
This can avoid suspending the outer critical section only to acquire another
lock. It does not make callbacks safe. If the helper can run arbitrary code,
the caller must still allow for re-entry and recheck mutable state afterward.
A typical wrapper and lock-held helper have this form:
static PyObject *
operation_lock_held(MyObject *self)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
/* Read or update protected state. */
...
}
static PyObject *
operation(MyObject *self)
{
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(self);
result = operation_lock_held(self);
Py_END_CRITICAL_SECTION();
return result;
}
The helper must not reacquire self's critical section. Its name and
assertion make the caller's locking responsibility explicit. Calls inside
the helper can still suspend the section or invoke arbitrary code, so the
same revalidation rules apply.
Argument Clinic @critical_section¶
For a Clinic-defined method, getter, or setter whose operation is protected
by its receiver, put @critical_section on the Clinic declaration. Argument
Clinic then generates the entry-point wrapper that acquires the critical
section and calls the _impl function while it is held. This places locking
at the API boundary, keeps the implementation focused on its operation,
and avoids repeated free-threading boilerplate while keeping the existing
signature and behavior.
Do not add Py_BEGIN_CRITICAL_SECTION again inside an implementation
reached through such an annotated wrapper. If a helper must be called both
from a protected Clinic entry point and elsewhere, make its locking contract
explicit - for example, use a lock_held helper and arrange for the caller
to acquire the section - rather than nesting ad hoc wrappers.
Use manual locking only where Clinic cannot describe the required object
or call boundary; otherwise convert the entry point, including getsets, to
Clinic and let the directive own the acquisition. The directive locks self
by default; it can name arguments instead, but lock only what the operation
actually mutates - locking an argument as well needs justification.
One accepted exception to converting: when Clinic would force duplicating a docstring shared across entry points, a manual wrapper around the impl is the preferred approach.
Atomics and memory ordering¶
needed or not, asymmetric, ordering, RMW
Whether an access must be atomic¶
Decide from what other threads can do to the state, not from the generated code. A data race is undefined behavior in C even when it looks harmless on current hardware or survives stress testing. Plain accesses are safe when the state is thread-local, immutable after publication, or protected by a lock that every competing access holds. If accesses can overlap, add synchronization; prefer a critical section or mutex unless measured performance requires a lock-free design.
Correct lock-free code requires reasoning about every competing access, object lifetime, publication, and memory ordering. Passing tests does not prove that an atomic protocol is correct. See Memory ordering is wrong for CPython-specific guidance and Mara Bos, Rust Atomics and Locks for a general introduction to atomics and memory ordering.
Locks are usually easier to reason about because they protect the invariant
of an operation, not just individual field accesses. In unregister_task,
for example, checking and changing links in a shared task list belongs
under the same lock; making each link access atomic would still let another
thread see the list part-way through an update. The PyMutex used by Python
critical sections is a fast, lightweight mutex based on the WebKit mutex
design. Do not assume that replacing it with atomics is an optimization. Use
atomics when the state really can be accessed independently and profiling
shows that locking is a meaningful cost or a scaling bottleneck for shared
reads.
The cost of an atomic depends on the operation, memory order, and CPU architecture. Naturally aligned relaxed loads and stores generally compile to ordinary machine loads and stores, including on AMD64. Acquire loads and release stores also commonly need no extra CPU instruction on AMD64, but impose ordering constraints and can require stronger instructions on architectures with weak memory ordering, such as ARM64. Sequentially consistent operations, explicit fences, and atomic read-modify-write operations may require more expensive serialization. Even relaxed atomics can inhibit compiler optimizations such as coalescing adjacent stores, and writes to heavily shared cache lines can cause cache-line contention.
Hot accessors sometimes justify a design that keeps reads plain and
moves synchronization to a rare writer. Py_TYPE, for example, is an
extremely common operation, while changing an object's type is rare. CPython
therefore keeps Py_TYPE as a plain load and uses a stop-the-world pause in
object_set_class, ensuring that readers are at safe points before the type
is changed. Making both Py_TYPE and Py_SET_TYPE atomic would remove the
C data race between those accesses, but would add cost to every type read
and would not by itself ensure that a thread is no longer acting on the old
type. See Stop-the-world where a lock would
do for the access pattern that can
justify this exceptional design.
Do not add atomics merely to quiet TSan or as the default response to shared state. When a plain access is intentional, document why it is safe, such as a lock being held, immutability after publication, or synchronization being performed by a rare writer.
One side of the access is atomic, the other is not¶
A lock on the writer does not make a concurrent unlocked reader safe. If a field is read with an atomic load, a plain store to the same field can still overlap it and form a data race. The reverse mix is just as unsafe: an atomic store does not protect a plain load. Both sides of a potentially concurrent access must follow the same synchronization scheme.
For a field that needs lock-free reads, use atomic operations for every
concurrent read and write; ob_size, for example, needs atomic stores as
well as atomic loads. Use relaxed atomics only when no ordering with other
memory is needed. Publishing a pointer or flag that makes earlier writes
visible requires stronger ordering. See Memory ordering is
wrong. Relaxed loads and stores are generally
cheaper than a critical section.
A PyObject* field is harder: a setter like Py_XSETREF must also release
the old reference, and an atomic store alone cannot do that safely. Do not
respond by making only the getter atomic. Either use a full atomic update
protocol or put both reads and writes under a critical section; the critical
section is usually simpler and is the right choice when the path is not
performance-critical.
Memory ordering is wrong¶
Pick the ordering from what the access must make visible. When installing a pointer to newly initialized data -- a resized array's replacement buffer, an object being stored into a list -- do the initialization first and publish the pointer with a "release" store. This ensures every prior write is visible before the pointer is. A reader that loads the pointer and then reads the data behind it needs the matching "acquire" load. The pair guarantees that a reader that sees the new pointer also sees the contents written before it was published.
Do not validate memory ordering only by testing on AMD64. For ordinary loads and stores, acquire and release operations commonly use the same CPU instructions as relaxed operations on AMD64, although their compiler-ordering constraints still differ. A missing acquire or release may remain hidden there and fail on an ARM64 system such as Apple Silicon or AWS Graviton.
Run a test that performs both sides of the synchronization protocol under ThreadSanitizer. The test must make the potentially conflicting operations overlap and reach the accesses whose ordering is in question. TSan can report a missing happens-before relationship without waiting for the hardware to expose it. However, it examines only executed paths and does not detect every logical error involving atomics. A clean TSan run does not prove that the memory ordering is correct. Choose the ordering from the required visibility.
Do not reach for stronger orderings by default. Moving already-published
items around, as in list's ins1() or heapq's siftdown, publishes nothing
new, so "relaxed" is enough. A "release" store is more expensive than a
relaxed store on ARM64.
Using seq_cst by default is a code smell. It often means the protocol's
actual ordering requirements have not been identified. Use it only when
correctness requires a single total order among the sequentially consistent
operations. Otherwise, select the weakest ordering that provides the
required visibility and ordering.
Read-modify-write is not atomic¶
A read-modify-write must be protected as one operation. If two threads both
load a shared value, compute a replacement, and store it, one update can
silently overwrite the other. ++, +=, and bit-mask updates are
load-then-store operations even when written as a single C expression.
Prefer putting the complete update under a critical section for per-object state or a mutex for independently owned shared state. This is usually the clearest and least error-prone solution, especially when the update involves multiple fields, reference ownership, or other invariants. Every competing access must follow the same locking discipline; locking only one update path does not make unlocked accesses safe.
Use an atomic read-modify-write only when the state is an independent scalar
and measured performance justifies the additional complexity. In that case,
use a primitive that performs the whole update atomically: _Py_atomic_add_*
for a counter, or atomic AND or OR for a bit mask. An atomic load followed by
an atomic store is still two separate operations and can still lose an
intervening update. If no primitive expresses the operation, a
compare-exchange loop may be required, including careful handling of retries,
memory ordering, and object lifetime.
Replacing a PyObject* field illustrates why locking should be the default.
Py_XSETREF must replace the pointer and decref the old reference safely;
making only the store atomic is insufficient, while a correct lock-free
replacement requires a more complicated compare-exchange protocol. The
func_code review therefore kept the whole replacement under a critical
section. A hot, independent scalar such as a bit mask may instead justify an
atomic operation, as with the atomic AND used to clear a dict watcher bit.
Reference handling¶
borrowed refs, Py_NewRef, tp_iternext, the FT model
Borrowed reference is not safe here¶
Obtain an owned reference before releasing the lock that protects a borrowed
pointer. After the lock is released, another thread can remove the value,
drop its last reference, and free it. A later Py_INCREF() is too late
because it may access freed memory.
Prefer an API that returns a new reference, such as PyList_GetItemRef(), and
handle failure if concurrent mutation removed the item. When reading under a
critical section, call Py_NewRef() before leaving the section. Keep stealing
operations explicit: PyList_SET_ITEM() and _PyList_AppendTakeRef() require
an owned reference to transfer.
Optimistic lock-free code needs a safe acquisition protocol, not a direct
incref of the loaded pointer. Deferred or immortal values can be used under
their specific lifetime rules. Otherwise use _Py_TryXGetRef() or
_Py_TryIncrefCompare(), and retry under the object's critical section if it
fails.
tp_iternext and shared iterators¶
The default goal for concurrent use of one iterator is crash-freedom. Two
threads calling tp_iternext on the same iterator must not access freed
memory, corrupt reference counts, or damage the iterator or its container.
They do not necessarily need to receive each item exactly once or in a
predictable order.
Concurrent calls may repeat, skip, or interleave values when the iterator's contract allows that behavior. Callers that need a specific division of the input can provide their own synchronization. Array iteration follows the list model: several threads can consume the same iterator without crashing, but they may receive repeated values.
Crash-freedom still requires enough synchronization to protect memory and
reference ownership. Updates to the iterator's position must not cause an
invalid access. A value taken from a mutable container must remain alive
while it is used. If memory safety depends on state from both the iterator
and its container remaining consistent, protect both with
Py_BEGIN_CRITICAL_SECTION2().
Do not fully serialize tp_iternext only because concurrent calls are
possible. Add stronger synchronization when the iterator has a useful
shared-consumption contract, such as guaranteeing that each item is returned
only once, and when that guarantee provides enough value to justify its
performance and implementation cost. Match comparable built-in iterators
unless the type has a reason to promise more.
In the free-threaded build, do not mark exhaustion by clearing the iterator's
owning reference to its container. Two threads can reach exhaustion and both
release the reference. Keep the container reference until the iterator is
destroyed. Record exhaustion in separate state, such as the iterator's
position. itertools.cycle and reversed use this pattern.
Deferred reference counting and VM stack references¶
Deferred reference counting changes when an object can be destroyed. An ordinary object can be destroyed when its reference count reaches zero. An object using deferred reference counting may remain alive until a later garbage-collection pass.
This delay can improve performance by avoiding reference-count contention on
commonly shared objects. It can also change visible behavior. Do not enable
deferred reference counting for an object when correctness depends on a
__del__ method or weak-reference callback running as soon as the reference
count reaches zero.
Deferred reference counting does not remove the normal ownership rules for
PyObject* references. Code must still obtain an owned reference when the
object could otherwise be removed or freed. Immortal objects have another
special lifetime: they are never destroyed. When correctness depends on an
object being deferred or immortal, make that assumption explicit in the
code.
The following guidance applies only to code in the interpreter VM. Most
CPython code, including extension modules, should continue to use ordinary
PyObject* ownership APIs. _PyStackRef is the internal representation used
by the evaluation stack and frame locals. See InternalDocs/stackrefs.md
when working in Python/bytecodes.c, the generated opcode evaluator, or
helpers that directly produce values for the evaluation stack.
A deferred _PyStackRef keeps its object alive only after the reference is
stored somewhere that the garbage collector scans. This includes the
interpreter evaluation stack and frame locals such as
_PyInterpreterFrame.localsplus. A _PyStackRef held only in a C local
variable is not visible to the garbage collector. If no visible reference
remains, a call that triggers garbage collection can allow the object to be
destroyed.
Code generated from bytecodes.c normally writes opcode results directly to
the interpreter evaluation stack. This makes deferred _PyStackRef values
visible to the garbage collector. A hand-written C helper does not get this
protection automatically. Before it calls any function that can run Python
code or trigger garbage collection (an "escaping call"), it must write the
result to the evaluation stack or another location scanned by the garbage
collector. Do not leave the only reference in a C local variable. The
garbage collector cannot see that reference and may destroy the object. This
requires extra care in opcode paths such as LOAD_ATTR, where descriptor
lookup can run arbitrary Python code.
Lock choice and discipline¶
primitive choice, lock order, lazy init, scope
Wrong synchronization mechanism¶
Choose synchronization from the state that must stay unchanged for the whole
operation. For per-object state, start with the object's critical section.
For shared state owned by a module, interpreter, or external library, start
with a PyMutex at that same scope. These choices usually make the ownership
and protected fields easier to understand than atomics do.
Use an atomic operation only for independent scalar state when measured performance justifies the extra complexity. See Whether an access must be atomic and Read-modify-write is not atomic. If an operation must preserve a relationship among fields or safely replace an owned reference, protect the complete operation with a lock.
Check what can run while a raw PyMutex is held. Unlike a Python critical
section, it is not suspended to avoid re-entrancy or lock-order deadlocks.
Its waiting path cooperates with thread-state detachment, so waiting for a
PyMutex does not by itself deadlock with the GIL or the free-threaded
garbage collector. This does not make the mutex re-entrant or prevent cycles
among mutexes. Do not hold it across finalizers, callbacks, or conversions
such as __index__ without a clear deadlock argument. A per-object PyMutex
can still be appropriate for plain memory work that cannot call Python, as
in compression code.
Treat stop-the-world as exceptional, not as an ordinary synchronization
mechanism. Use it only for a genuinely global transition whose readers do
not take a common lock. Changing an object's type is one example: readers use
Py_TYPE() throughout the runtime, so locking only the writer does not stop
a thread from continuing to act on the old type. See Unsafe work while the
world is stopped and Stop-the-world
where a lock would do.
Lock ordering and deadlock¶
When one operation must lock two Python objects together, prefer
Py_BEGIN_CRITICAL_SECTION2() to two manually nested critical sections.
Nested Python critical sections avoid lock-order deadlocks by releasing an
active outer section when acquisition blocks. That also means another thread
can change the outer object during the inner section. The two-object form
keeps the relationship between both objects protected as one operation.
A single critical section can also work well for shared state owned by one
Python object, such as module-level state associated with a module object. If
every access uses that owner's lock, re-entrant acquisition does not deadlock
as a raw PyMutex can. Re-entry may still modify the protected state, so
revalidate any assumptions after a call that can invoke arbitrary code. Do
not use a per-interpreter owner's lock for process-global state shared across
interpreters. See Critical section suspension
semantics.
Treat plain PyMutex locks differently. They are not re-entrant. A helper
that acquires a mutex already held by its caller can deadlock immediately.
Name lock-held helpers clearly, document which lock the caller owns, and do
not reacquire it inside the helper.
Avoid holding a plain mutex across a call that can invoke arbitrary Python code or an external callback. Re-entrant code may take the same mutex or another mutex in the opposite order. If the call must occur while object state is protected, consider a Python critical section or restructure the operation so the callback runs outside the raw lock.
Lock lives at the wrong scope¶
Put the lock at the same scope as the shared resource. Ask whether the state belongs to one object, one interpreter, one process, or one thread. Then make every conflicting access use a lock that all users of that resource share.
A per-object critical section or a mutex in module state cannot protect
process-global state across subinterpreters. If a non-thread-safe libc or
external-library interface uses global storage, use a process-global
PyMutex. Keep the mutex around only the calls that share that storage when
the library's contract permits it. Conversely, put truly thread-local state
in PyThreadState instead of building a shared cache protected by a mutex.
Do not serialize an entire extension merely because one part is not thread safe. Identify which operations conflict. For example, readline history-file operations may need one lock, while updating an unrelated Python hook may be able to proceed concurrently. The lock should cover all accesses to its resource and no unrelated work.
Stop-the-world¶
__PyEval_StopTheWorld misuse and overuse_
Unsafe work while the world is stopped¶
Stop-the-world is a big hammer. It pauses every affected interpreter thread, including threads doing unrelated work. Frequent pauses can destroy multi-threaded performance. Do not choose stop-the-world only because it is simpler than deciding which state to lock.
Prefer a critical section for per-object state or a PyMutex for other
narrowly owned shared state. Use stop-the-world only for a whole-interpreter
change that cannot be protected efficiently with a narrower mechanism. One
case that can justify it is a very rare update to state read on many hot
paths. Changing an object's type follows this pattern. The rare
__class__ assignment stops the world so that common Py_TYPE() reads can
remain plain and inexpensive.
Stop-the-world is a general runtime mechanism. The garbage collector is one
user of it, but stopping the world does not mean that garbage collection is
in progress. Put work that requires all other threads to be paused directly
between _PyEval_StopTheWorld() and _PyEval_StartTheWorld(). Do not infer
that state from GC flags.
Keep the stopped region short. Structure the operation in this order:
- Perform validation and other preparation that does not require the world to be stopped.
- Stop the world.
- Perform only the global state change that requires all other threads to be paused.
- Start the world again on every control-flow path.
- Perform cleanup, reference releases, and error reporting after other threads can run again.
Code inside the stopped region must not wait for a mutex or critical-section lock. A suspended thread may hold that lock and cannot run to release it. Use direct field access or a suitable lock-held helper when the stop-the-world pause already provides the required exclusion.
Code inside the stopped region must also avoid running arbitrary Python
code. In particular, Py_DECREF() can invoke a finalizer when it releases
the last reference. Error reporting can allocate memory or re-enter the
interpreter. If the protected operation detects an error, record the failure,
restart the world, and then release references or report the error.
Stop-the-world where a lock would do¶
Do not stop every interpreter thread to protect state owned by one object or
subsystem. Use the narrowest shared mechanism: a critical section for
per-object state, or a PyMutex for independently owned shared state. This
lets unrelated threads continue running.
Consider stop-the-world for the opposite access pattern: a whole-interpreter value that is read on many hot paths and changed very rarely. A pause around the rare GC-threshold update was suggested so all ordinary readers could remain unsynchronized. Compare that design with the cost and complexity of adding synchronization to every read.
Make the tradeoff explicit. A pause is not a shortcut for avoiding lock design, and a lock is not automatically cheaper when it burdens ubiquitous readers. Avoid stop-the-world in diagnostics and fatal-error paths, where a deadlock can suppress the report itself.
Tests¶
missing, cannot race, gating, TSan, build-only failures
Needs a test¶
A regression test is normally expected with every bug fix. The test should exercise the code path and conflicting operations that caused the bug. Without the fix, it should demonstrate the bug by failing or producing the expected TSan report. With the fix, it should pass reliably across supported hardware. Run it against both versions before submitting the PR. A test that also passes without the fix does not protect against the bug returning.
Put the test near related tests. A test that is specific to the free-threaded
build generally belongs in Lib/test/test_free_threading/. If the module
already has a test_free_threading_* file, class, or section, add the test
there instead. Follow the existing organization rather than creating another
location for the same module.
Useful hints for concurrency regression tests:
- Start with the reproducer from the issue when it exercises the same code path. Reduce it to the smallest reliable unit test.
- Make the conflicting operations overlap. A barrier or other explicit coordination is usually better than relying only on timing.
- If normal execution does not reliably demonstrate the race, run the unfixed and fixed versions under TSan. The test must still reach the intended concurrent path.
- Assert the behavior the API actually promises. Some operations promise only crash-freedom. Others must preserve a stronger property, such as the heap invariant. Do not assert one exact ordering when thread scheduling makes several results valid.
- Use
threading_helper.catch_threading_exceptionwhen exceptions raised in worker threads might otherwise be missed. - Cover separate changed implementations separately. For example, bounded
and unbounded
lru_cachepaths need separate tests when both were changed. - Mark tests that start threads with
@threading_helper.requires_working_threading()so they skip platforms without working thread support.
The test cannot actually hit the race¶
Make the two conflicting operations overlap on purpose. Start workers with a
barrier, then repeat the exact read and mutation involved in the bug. For
example, subscript an array while another thread repeatedly resizes it.
threading_helper.run_concurrently() already provides a barrier when its
interface fits the test. When the bug requires a specific precondition or
handoff between threads, coordinate it with a Barrier, Event, or
Condition instead of time.sleep(). A sleep does not guarantee that
another thread has reached the required point and can make the test
platform-dependent or flaky.
Do not set a stop event immediately after Thread.start(); workers may exit
before they contend. Prefer a fixed number of short operations, or let the
writer set the event after it completes its loop. Repeated small mutations
create more useful interleavings than one large mutation. Keep the test fast
enough for normal CI; reviews have used a target below 100 ms.
Assert an invariant that holds under every allowed schedule, not one exact sequence of results. Confirm that the setup reaches the synchronization path changed by the fix. Object-sharing transitions can select a different locked path and accidentally hide the race. Finally, run the test against the unfixed code. It should fail, crash, or produce the expected TSan report.
Test placement and gating¶
Put the regression beside the module's other free-threading tests. Use its
existing test_free_threading_* class or group, or place a dedicated test in
Lib/test/test_free_threading/. Follow the local organization instead of
creating a third location for the same module.
Every test that starts threads needs
@threading_helper.requires_working_threading() so it skips platforms such
as WASI. Use threading_helper lifecycle helpers such as start_threads()
or reap_threads, and add @support.requires_resource("cpu") when the test
is intentionally CPU intensive.
Gate on the behavior, not the diagnostic tool. Use
support.Py_GIL_DISABLED only when the test is meaningful solely in a
free-threading-capable build. Do not substitute a TSan check: TSan can run on
the default build and can find races there. If the regression is valid with
the GIL enabled, run it in both builds.
A free-threading-capable build can be started with its GIL enabled. When the
test specifically requires the GIL to be off, check
sys._is_gil_enabled() at runtime and give a precise skip reason.
ThreadSanitizer¶
Run a race regression under TSan instead of waiting for a reproducible crash. Verify that the test reaches the intended concurrent path. Where practical, confirm locally that the unfixed code reports the race and that the fixed code runs cleanly. If TSan stays quiet in both versions, check whether object sharing, a lock, or another condition sent the test down a different path. For build and usage instructions, see Using Thread Sanitizer to validate and test thread safety.
Add tests that create their own concurrent workload to TSAN_TESTS in
Lib/test/libregrtest/tsan.py. Add a test to TSAN_PARALLEL_TESTS when the
regression-test runner must execute it concurrently with
--parallel-threads. These are different ways to create concurrency. Choose
the one that reproduces the race the test is meant to cover.
TSan runs in CI are expected to report no errors. The suppression files for both the GIL and free-threaded builds currently contain no active suppressions. Treat every new report as a CPython bug until it has been investigated. Fix the cause rather than adding a suppression or skipping the workload under TSan.
A regression test that exposes an existing TSan report must be added together
with the fix. Do not first add it to TSAN_TESTS or TSAN_PARALLEL_TESTS and
leave CI failing. Confirm locally that the test reports the race without the
fix and runs cleanly with the fix.
Performance¶
default-build regression, scaling and contention
Regression in the default build¶
Keep the traditional GIL build's hot path unchanged unless sharing the new
code has negligible cost. The GIL already protects ordinary object access in
that build. If a free-threading fix adds real synchronization or obscures a
previously simple path, use #ifdef Py_GIL_DISABLED and leave the original
GIL implementation easy to read.
Check the generated cost before adding a separate branch. A Python critical
section is a no-op in the GIL build, and some free-threading atomic wrappers
compile to an ordinary access there. In those cases, an #ifdef may add code
without improving performance.
Also check whether the concurrent use is worth supporting. Do not slow common
single-threaded operations to provide stronger results for a use case that
still needs caller-side locking. State the promised behavior first, then
benchmark both builds. Keep one implementation when its cost is negligible.
If the GIL build regresses, put only the costly free-threading operation
behind #ifdef Py_GIL_DISABLED rather than duplicating more code than
necessary.
Scaling and contention¶
First decide whether concurrent use of the same object is useful. Do not assume that every operation must scale well when many threads call it at once. Several threads consuming one iterator, for example, receive interleaved parts of the input. They usually still need their own coordination to make the result useful.
Start with the simplest correct design. This is often one critical section or mutex that serializes access to the shared state. A simple locking design is easier to review, test, and maintain. Do not add sharded locks, atomics, or lock-free paths only because concurrent calls are possible.
Consider a more complex design when there is evidence that the simple design is a problem. Evidence may come from real applications, an established scaling benchmark, or an operation that is already known to run concurrently in common workloads. A microbenchmark for an unlikely use case is not enough by itself.
When scaling matters, measure performance as threads are added. A lock that is cheap without contention can become a serialization point when many threads use the same object, module state, or external library. Use a representative multi-threaded workload. A single-thread benchmark cannot show contention.
Only then consider narrowing or splitting the lock. Lock the smallest resource that needs protection and hold the lock only for the required part of the operation. For example, serialize calls into a non-thread-safe library without also serializing unrelated Python work. Preserve an existing lock-free read path when its safety rules are clear and measurements show that it matters.
More complex concurrency control must provide a measured benefit large enough to justify its additional correctness and maintenance cost. If real workloads do not suffer from contention, keep the simple design.
Observable behaviour¶
behaviour change, and why code is already safe
Define the thread-safety contract¶
Before changing synchronization, state what behavior the operation should provide. Compare the behavior before and after the PR. For free-threading changes, also compare the GIL and free-threaded builds. If both C and pure-Python implementations exist, check whether they still provide the same behavior.
Adding a lock is not only an implementation detail. It can make operations run one at a time when they previously overlapped. It can prevent callers from observing intermediate state. It can also change when an exception is raised. These changes may be visible to users.
Decide which guarantee is useful for the operation:
- Must concurrent calls only avoid crashes and memory corruption?
- Must each individual access be safe?
- Must the whole operation see one consistent version of the object?
- Must concurrent mutations be fully serialized?
- Does the caller still need its own lock to obtain a meaningful result?
Do not provide the strongest guarantee automatically. Stronger synchronization can cost more and may not make concurrent use useful. Crash-freedom is enough for some operations. Other operations, such as updates to several related fields, may require full serialization.
A safe individual access does not guarantee a consistent result for a whole
operation. For example, a lock-free list.index() can run while another
thread calls list.reverse(). Each element read can be memory-safe, while
the search still observes some elements before the reverse and others after
it. This is an intermediate result. It is not necessarily a torn machine
read or a C data race.
State which entry points provide the guarantee. A supported Python operation
may be thread-safe even when a lower-level C API is not. For example,
obj.__class__ = new_type can be safe while direct concurrent use of
Py_SET_TYPE() remains unsupported. Do not imply that every path is safe
when only one path performs the required synchronization.
Document a public thread-safety guarantee when users need to rely on it. Describe weaker guarantees accurately. Also explain the intended behavior in the PR so reviewers can decide whether the synchronization implements the right contract rather than only hiding a reported race.
Why the code is already safe, with no mechanism named¶
Do not add synchronization only because code can run without the GIL. For
each field, explain why a conflicting access cannot overlap. Common reasons
are: the object is immutable after publication, the state belongs to one
thread, construction finishes before publication, or destruction begins only
after other references are gone. For example, tp_dealloc cannot run
concurrently on the same reachable object.
Check the exact operation rather than making a broad claim about its
surrounding function. UNPACK_SEQUENCE can read a tuple without locking
because tuple contents are immutable. The list case must keep the size check
and element pushes together because another thread can resize the list.
Likewise, an abstract sequence API may already provide the required safety,
while direct access to list internals needs explicit protection.
Separate a possible performance loss from a correctness failure. Ask what happens if the state changes between a check and its later use. It may be acceptable to do extra work or miss an optimization. It is never acceptable to access freed memory, use an invalid index, lose a reference, or perform an update using assumptions that are no longer true. In those cases, use synchronization or check the state again before using it. Put any non-obvious reason that the change is harmless in a comment beside the code.
Dual-build portability¶
Py_GIL_DISABLED guards and the FT* wrappers_
Build guards and the FT_* wrappers¶
Keep the common source path when the two builds implement the same
operation. Use an FT_* wrapper for an access that must be atomic only
without the GIL, rather than spelling a raw _Py_atomic_* operation at each
call site: the wrapper supplies the free-threaded operation and expands to
the ordinary operation in the default build. The same applies to the other
FT_* wrappers for locks, weak references, and inlining. This preserves
the GIL build's existing code and avoids making its hot paths pay for
synchronization they do not need.
Use #ifdef Py_GIL_DISABLED when the representation or algorithm genuinely
differs, and arrange it so the default branch remains recognizable as
the original simple code. Do not add a build split solely to wrap a
primitive that has a suitable FT_* form, but do split a call site if
the wrapper cannot preserve its required semantics. In particular, check
return values and side effects: an atomic fetch-add and value += n need
not have the same result value.
Do not guard the critical-section macros either: Py_BEGIN_CRITICAL_SECTION
already compiles to nothing in the default build. The one sanctioned
exception to "just use the wrapper" is a handful of hot paths
(new_reference(), _Py_IsOwnedByCurrentThread()) where stores are atomic
only in _Py_THREAD_SANITIZER builds, so plain stores to adjacent fields
can coalesce; that pattern needs a measured, performance-sensitive path to
justify it and should not be blindly copied elsewhere.