Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Doc/c-api/threads.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,61 @@ For example::
If the interpreter finalized before ``PyThreadState_Swap`` was called, then
``interp`` will be a dangling pointer!

Reusing a thread state across repeated calls
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Creating and destroying a :c:type:`PyThreadState` is not free, and is
more expensive on a :term:`free-threaded build`. If a non-Python thread
calls into the interpreter many times, creating a fresh thread state on
every entry and destroying it on every exit is a performance
anti-pattern. Instead, create the thread state once (when the native
thread starts, or lazily on its first call into Python), attach and
detach it around each call, and destroy it when the native thread
exits::

/* Thread startup: create the state once. */
PyThreadState *tstate = PyThreadState_New(interp);

/* Per-call: attach, run Python, detach. */
PyEval_RestoreThread(tstate);
result = CallSomeFunction();
PyEval_SaveThread();

/* ... many more calls ... */

/* Thread shutdown: destroy the state once. */
PyEval_RestoreThread(tstate);
PyThreadState_Clear(tstate);
PyThreadState_DeleteCurrent();

The equivalent with the :ref:`PyGILState API <gilstate>` keeps an *outer*
:c:func:`PyGILState_Ensure` outstanding for the thread's lifetime, so
nested Ensure/Release pairs never drop the internal nesting counter to
zero::

/* Thread startup: create and pin the state. */
PyGILState_STATE outer = PyGILState_Ensure();
PyThreadState *saved = PyEval_SaveThread();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, what's the point of detaching the thread state again? PyGILState_Ensure is able to handle nested calls.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see how this is a little confusing as written (i oversimplified). check the latest version (commit added).

open question... this is to work around the PyGILState recursion counter and basically "abuse" it as a way to prevent _Release from destroying the Python Thread State it creates internally. I don't really want to mention that detail but maybe to avoid questions this should?


/* Per-call: the thread state already exists. */
PyGILState_STATE inner = PyGILState_Ensure();
result = CallSomeFunction();
PyGILState_Release(inner);

/* ... many more calls ... */

/* Thread shutdown: unpin and destroy the state. */
PyEval_RestoreThread(saved);
PyGILState_Release(outer);

The embedding code must arrange for the shutdown sequence to run before
the native thread exits, and before :c:func:`Py_FinalizeEx` is called.
If interpreter finalization begins first, the shutdown
:c:func:`PyEval_RestoreThread` call will hang the thread (see
:c:func:`PyEval_RestoreThread` for details) rather than return. If the
native thread exits without running the shutdown sequence, the thread
state is leaked for the remainder of the process.

.. _gilstate:

Legacy API
Expand Down
Loading