mirror of
https://github.com/python/cpython.git
synced 2026-04-13 23:31:02 +00:00
gh-145254: Add thread safety annotation in docs (#145255)
This commit is contained in:
parent
d19de375a2
commit
0dce4c6eab
4 changed files with 195 additions and 0 deletions
|
|
@ -569,6 +569,7 @@
|
|||
# Relative filename of the data files
|
||||
refcount_file = 'data/refcounts.dat'
|
||||
stable_abi_file = 'data/stable_abi.dat'
|
||||
threadsafety_file = 'data/threadsafety.dat'
|
||||
|
||||
# Options for sphinxext-opengraph
|
||||
# -------------------------------
|
||||
|
|
|
|||
19
Doc/data/threadsafety.dat
Normal file
19
Doc/data/threadsafety.dat
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Thread safety annotations for C API functions.
|
||||
#
|
||||
# Each line has the form:
|
||||
# function_name : level
|
||||
#
|
||||
# Where level is one of:
|
||||
# incompatible -- not safe even with external locking
|
||||
# compatible -- safe if the caller serializes all access with external locks
|
||||
# distinct -- safe on distinct objects without external synchronization
|
||||
# shared -- safe for concurrent use on the same object
|
||||
# atomic -- atomic
|
||||
#
|
||||
# Lines beginning with '#' are ignored.
|
||||
# The function name must match the C domain identifier used in the documentation.
|
||||
|
||||
# Synchronization primitives (Doc/c-api/synchronization.rst)
|
||||
PyMutex_Lock:shared:
|
||||
PyMutex_Unlock:shared:
|
||||
PyMutex_IsLocked:atomic:
|
||||
|
|
@ -13,6 +13,88 @@ For general guidance on writing thread-safe code in free-threaded Python, see
|
|||
:ref:`freethreading-python-howto`.
|
||||
|
||||
|
||||
.. _threadsafety-levels:
|
||||
|
||||
Thread safety levels
|
||||
====================
|
||||
|
||||
The C API documentation uses the following levels to describe the thread
|
||||
safety guarantees of each function. The levels are listed from least to
|
||||
most safe.
|
||||
|
||||
.. _threadsafety-level-incompatible:
|
||||
|
||||
Incompatible
|
||||
------------
|
||||
|
||||
A function or operation that cannot be made safe for concurrent use even
|
||||
with external synchronization. Incompatible code typically accesses
|
||||
global state in an unsynchronized way and must only be called from a single
|
||||
thread throughout the program's lifetime.
|
||||
|
||||
Example: a function that modifies process-wide state such as signal handlers
|
||||
or environment variables, where concurrent calls from any threads, even with
|
||||
external locking, can conflict with the runtime or other libraries.
|
||||
|
||||
.. _threadsafety-level-compatible:
|
||||
|
||||
Compatible
|
||||
----------
|
||||
|
||||
A function or operation that is safe to call from multiple threads
|
||||
*provided* the caller supplies appropriate external synchronization, for
|
||||
example by holding a :term:`lock` for the duration of each call. Without
|
||||
such synchronization, concurrent calls may produce :term:`race conditions
|
||||
<race condition>` or :term:`data races <data race>`.
|
||||
|
||||
Example: a function that reads from or writes to an object whose internal
|
||||
state is not protected by a lock. Callers must ensure that no two threads
|
||||
access the same object at the same time.
|
||||
|
||||
.. _threadsafety-level-distinct:
|
||||
|
||||
Safe on distinct objects
|
||||
------------------------
|
||||
|
||||
A function or operation that is safe to call from multiple threads without
|
||||
external synchronization, as long as each thread operates on a **different**
|
||||
object. Two threads may call the function at the same time, but they must
|
||||
not pass the same object (or objects that share underlying state) as
|
||||
arguments.
|
||||
|
||||
Example: a function that modifies fields of a struct using non-atomic
|
||||
writes. Two threads can each call the function on their own struct
|
||||
instance safely, but concurrent calls on the *same* instance require
|
||||
external synchronization.
|
||||
|
||||
.. _threadsafety-level-shared:
|
||||
|
||||
Safe on shared objects
|
||||
----------------------
|
||||
|
||||
A function or operation that is safe for concurrent use on the **same**
|
||||
object. The implementation uses internal synchronization (such as
|
||||
:term:`per-object locks <per-object lock>` or
|
||||
:ref:`critical sections <python-critical-section-api>`) to protect shared
|
||||
mutable state, so callers do not need to supply their own locking.
|
||||
|
||||
Example: :c:func:`PyList_GetItemRef` can be called from multiple threads on the
|
||||
same :c:type:`PyListObject` - it uses internal synchronization to serialize
|
||||
access.
|
||||
|
||||
.. _threadsafety-level-atomic:
|
||||
|
||||
Atomic
|
||||
------
|
||||
|
||||
A function or operation that appears :term:`atomic <atomic operation>` with
|
||||
respect to other threads - it executes instantaneously from the perspective
|
||||
of other threads. This is the strongest form of thread safety.
|
||||
|
||||
Example: :c:func:`PyMutex_IsLocked` performs an atomic read of the mutex
|
||||
state and can be called from any thread at any time.
|
||||
|
||||
|
||||
.. _thread-safety-list:
|
||||
|
||||
Thread safety for list objects
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
* Reference count annotations for C API functions.
|
||||
* Stable ABI annotations
|
||||
* Limited API annotations
|
||||
* Thread safety annotations for C API functions.
|
||||
|
||||
Configuration:
|
||||
* Set ``refcount_file`` to the path to the reference count data file.
|
||||
* Set ``stable_abi_file`` to the path to stable ABI list.
|
||||
* Set ``threadsafety_file`` to the path to the thread safety data file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -48,6 +50,15 @@ class RefCountEntry:
|
|||
result_refs: int | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, slots=True)
|
||||
class ThreadSafetyEntry:
|
||||
# Name of the function.
|
||||
name: str
|
||||
# Thread safety level.
|
||||
# One of: 'incompatible', 'compatible', 'safe'.
|
||||
level: str
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, slots=True)
|
||||
class StableABIEntry:
|
||||
# Role of the object.
|
||||
|
|
@ -113,10 +124,42 @@ def read_stable_abi_data(stable_abi_file: Path) -> dict[str, StableABIEntry]:
|
|||
return stable_abi_data
|
||||
|
||||
|
||||
_VALID_THREADSAFETY_LEVELS = frozenset({
|
||||
"incompatible",
|
||||
"compatible",
|
||||
"distinct",
|
||||
"shared",
|
||||
"atomic",
|
||||
})
|
||||
|
||||
|
||||
def read_threadsafety_data(
|
||||
threadsafety_filename: Path,
|
||||
) -> dict[str, ThreadSafetyEntry]:
|
||||
threadsafety_data = {}
|
||||
for line in threadsafety_filename.read_text(encoding="utf8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
# Each line is of the form: function_name : level : [comment]
|
||||
parts = line.split(":", 2)
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f"Wrong field count in {line!r}")
|
||||
name, level = parts[0].strip(), parts[1].strip()
|
||||
if level not in _VALID_THREADSAFETY_LEVELS:
|
||||
raise ValueError(
|
||||
f"Unknown thread safety level {level!r} for {name!r}. "
|
||||
f"Valid levels: {sorted(_VALID_THREADSAFETY_LEVELS)}"
|
||||
)
|
||||
threadsafety_data[name] = ThreadSafetyEntry(name=name, level=level)
|
||||
return threadsafety_data
|
||||
|
||||
|
||||
def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
|
||||
state = app.env.domaindata["c_annotations"]
|
||||
refcount_data = state["refcount_data"]
|
||||
stable_abi_data = state["stable_abi_data"]
|
||||
threadsafety_data = state["threadsafety_data"]
|
||||
for node in doctree.findall(addnodes.desc_content):
|
||||
par = node.parent
|
||||
if par["domain"] != "c":
|
||||
|
|
@ -126,6 +169,12 @@ def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
|
|||
name = par[0]["ids"][0].removeprefix("c.")
|
||||
objtype = par["objtype"]
|
||||
|
||||
# Thread safety annotation — inserted first so it appears last (bottom-most)
|
||||
# among all annotations.
|
||||
if entry := threadsafety_data.get(name):
|
||||
annotation = _threadsafety_annotation(entry.level)
|
||||
node.insert(0, annotation)
|
||||
|
||||
# Stable ABI annotation.
|
||||
if record := stable_abi_data.get(name):
|
||||
if ROLE_TO_OBJECT_TYPE[record.role] != objtype:
|
||||
|
|
@ -256,6 +305,46 @@ def _unstable_api_annotation() -> nodes.admonition:
|
|||
)
|
||||
|
||||
|
||||
def _threadsafety_annotation(level: str) -> nodes.emphasis:
|
||||
match level:
|
||||
case "incompatible":
|
||||
display = sphinx_gettext("Not safe to call from multiple threads.")
|
||||
reftarget = "threadsafety-level-incompatible"
|
||||
case "compatible":
|
||||
display = sphinx_gettext(
|
||||
"Safe to call from multiple threads"
|
||||
" with external synchronization only."
|
||||
)
|
||||
reftarget = "threadsafety-level-compatible"
|
||||
case "distinct":
|
||||
display = sphinx_gettext(
|
||||
"Safe to call without external synchronization"
|
||||
" on distinct objects."
|
||||
)
|
||||
reftarget = "threadsafety-level-distinct"
|
||||
case "shared":
|
||||
display = sphinx_gettext(
|
||||
"Safe for concurrent use on the same object."
|
||||
)
|
||||
reftarget = "threadsafety-level-shared"
|
||||
case "atomic":
|
||||
display = sphinx_gettext("Atomic.")
|
||||
reftarget = "threadsafety-level-atomic"
|
||||
case _:
|
||||
raise AssertionError(f"Unknown thread safety level {level!r}")
|
||||
ref_node = addnodes.pending_xref(
|
||||
display,
|
||||
nodes.Text(display),
|
||||
refdomain="std",
|
||||
reftarget=reftarget,
|
||||
reftype="ref",
|
||||
refexplicit="True",
|
||||
)
|
||||
prefix = sphinx_gettext("Thread safety:") + " "
|
||||
classes = ["threadsafety", f"threadsafety-{level}"]
|
||||
return nodes.emphasis("", prefix, ref_node, classes=classes)
|
||||
|
||||
|
||||
def _return_value_annotation(result_refs: int | None) -> nodes.emphasis:
|
||||
classes = ["refcount"]
|
||||
if result_refs is None:
|
||||
|
|
@ -342,11 +431,15 @@ def init_annotations(app: Sphinx) -> None:
|
|||
state["stable_abi_data"] = read_stable_abi_data(
|
||||
Path(app.srcdir, app.config.stable_abi_file)
|
||||
)
|
||||
state["threadsafety_data"] = read_threadsafety_data(
|
||||
Path(app.srcdir, app.config.threadsafety_file)
|
||||
)
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
app.add_config_value("refcount_file", "", "env", types={str})
|
||||
app.add_config_value("stable_abi_file", "", "env", types={str})
|
||||
app.add_config_value("threadsafety_file", "", "env", types={str})
|
||||
app.add_directive("limited-api-list", LimitedAPIList)
|
||||
app.add_directive("corresponding-type-slot", CorrespondingTypeSlot)
|
||||
app.connect("builder-inited", init_annotations)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue