Skip to content

Commit 24c05e2

Browse files
committed
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.
1 parent ab47892 commit 24c05e2

File tree

1 file changed

+55
-0
lines changed

1 file changed

+55
-0
lines changed

Doc/c-api/threads.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,61 @@ For example::
227227
If the interpreter finalized before ``PyThreadState_Swap`` was called, then
228228
``interp`` will be a dangling pointer!
229229

230+
Reusing a thread state across repeated calls
231+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
232+
233+
Creating and destroying a :c:type:`PyThreadState` is not free, and is
234+
more expensive on a :term:`free-threaded build`. If a non-Python thread
235+
calls into the interpreter many times, creating a fresh thread state on
236+
every entry and destroying it on every exit is a performance
237+
anti-pattern. Instead, create the thread state once (when the native
238+
thread starts, or lazily on its first call into Python), attach and
239+
detach it around each call, and destroy it when the native thread
240+
exits::
241+
242+
/* Thread startup: create the state once. */
243+
PyThreadState *tstate = PyThreadState_New(interp);
244+
245+
/* Per-call: attach, run Python, detach. */
246+
PyEval_RestoreThread(tstate);
247+
result = CallSomeFunction();
248+
PyEval_SaveThread();
249+
250+
/* ... many more calls ... */
251+
252+
/* Thread shutdown: destroy the state once. */
253+
PyEval_RestoreThread(tstate);
254+
PyThreadState_Clear(tstate);
255+
PyThreadState_DeleteCurrent();
256+
257+
The equivalent with the :ref:`PyGILState API <gilstate>` keeps an *outer*
258+
:c:func:`PyGILState_Ensure` outstanding for the thread's lifetime, so
259+
nested Ensure/Release pairs never drop the internal nesting counter to
260+
zero::
261+
262+
/* Thread startup: create and pin the state. */
263+
PyGILState_STATE outer = PyGILState_Ensure();
264+
PyThreadState *saved = PyEval_SaveThread();
265+
266+
/* Per-call: the thread state already exists. */
267+
PyGILState_STATE inner = PyGILState_Ensure();
268+
result = CallSomeFunction();
269+
PyGILState_Release(inner);
270+
271+
/* ... many more calls ... */
272+
273+
/* Thread shutdown: unpin and destroy the state. */
274+
PyEval_RestoreThread(saved);
275+
PyGILState_Release(outer);
276+
277+
The embedding code must arrange for the shutdown sequence to run before
278+
the native thread exits, and before :c:func:`Py_FinalizeEx` is called.
279+
If interpreter finalization begins first, the shutdown
280+
:c:func:`PyEval_RestoreThread` call will hang the thread (see
281+
:c:func:`PyEval_RestoreThread` for details) rather than return. If the
282+
native thread exits without running the shutdown sequence, the thread
283+
state is leaked for the remainder of the process.
284+
230285
.. _gilstate:
231286

232287
Legacy API

0 commit comments

Comments
 (0)