Skip to content

Commit 30a6bc5

Browse files
gpsheadZeroIntensity
authored andcommitted
gh-146219: Document reusing a thread state across repeated foreign-thread calls (GH-146221)
* Document reusing a thread state across repeated foreign-thread calls Add a subsection under "Non-Python created threads" explaining the performance cost of creating/destroying a PyThreadState on every Ensure/Release cycle and showing how to keep one alive for the thread's lifetime instead. * add a comma Co-authored-by: Peter Bierma <zintensitydev@gmail.com> --------- (cherry picked from commit 5717518) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
1 parent 0b273d8 commit 30a6bc5

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

Doc/c-api/threads.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,61 @@ a thread state that was previously attached for the current thread.
242242
.. seealso::
243243
:pep:`788`
244244

245+
.. _c-api-reuse-thread-state:
246+
247+
Reusing a thread state across repeated calls
248+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
249+
250+
Creating and destroying a :c:type:`PyThreadState` is not free, and is more
251+
expensive on a :term:`free-threaded build`. A foreign thread that calls into
252+
the interpreter many times -- for example, a worker thread in a native thread
253+
pool -- should avoid creating a fresh thread state on every entry and
254+
destroying it on every exit. Instead, set up one thread state when the thread
255+
starts (or lazily on its first call into Python), attach and detach it around
256+
each call, and tear it down once when the thread exits.
257+
258+
Manage the thread state explicitly with :c:func:`PyThreadState_New`, attaching
259+
and detaching it with :c:func:`PyEval_RestoreThread` and
260+
:c:func:`PyEval_SaveThread`. This happens in three distinct phases, at
261+
different points in the thread's life.
262+
263+
When the thread starts, create one thread state for it. ``interp`` is the
264+
target interpreter, captured by the code that created this thread while it held
265+
an attached thread state (for example via :c:func:`PyInterpreterState_Get`)::
266+
267+
PyThreadState *tstate = PyThreadState_New(interp);
268+
269+
Then, on each call into Python -- which may happen many times over the thread's
270+
life -- attach the thread state, make the Python C API calls that require it,
271+
and detach again so the thread does not hold the GIL while off doing non-Python
272+
work::
273+
274+
PyEval_RestoreThread(tstate);
275+
result = CallSomeFunction(); /* your Python C API calls go here */
276+
PyEval_SaveThread();
277+
278+
When the thread is finished calling into Python, destroy the thread state once::
279+
280+
PyEval_RestoreThread(tstate);
281+
PyThreadState_Clear(tstate);
282+
PyThreadState_DeleteCurrent();
283+
284+
The general-purpose entry points for calling in from a foreign thread --
285+
:c:func:`PyThreadState_Ensure` and the older :c:func:`PyGILState_Ensure` -- do
286+
*not* guarantee a persistent thread state: their thread-state lifetime is
287+
deliberately implementation-defined, so a matched acquire/release pair may
288+
create and destroy a thread state each time. Use :c:func:`PyThreadState_New`,
289+
as shown here, whenever you specifically want to reuse one thread state across
290+
calls.
291+
292+
The code that created the foreign thread must arrange for the shutdown sequence
293+
to run before the thread exits, and before :c:func:`Py_FinalizeEx` is called.
294+
If interpreter finalization begins first, the shutdown
295+
:c:func:`PyEval_RestoreThread` call will hang the thread rather than return (see
296+
:ref:`cautions-regarding-runtime-finalization`). If the thread exits without
297+
running the shutdown sequence, the thread state is leaked for the remainder of
298+
the process.
299+
245300
.. _c-api-attach-detach:
246301

247302
Attaching/detaching thread states

0 commit comments

Comments
 (0)