Python multi-thread multi-interpreter C API Python multi-thread multi-interpreter C API python python

Python multi-thread multi-interpreter C API


Sub interpreters in Python are not well documented or even well supported. The following is to the best of my undestanding. It seems to work well in practice.

Threre are two important concepts to understand when dealing with threads and sub interpreters in Python. First, the Python interpreter is not really multi threaded. It has a Global Interpreter Lock (GIL) that needs to be acquired to perform almost any Python operation (there are a few rare exceptions to this rule).

Second, every combination of thread and sub interpreter has to have its own thread state. The interpreter creates a thread state for every thread managed by it, but if you want to use Python from a thread not created by that interpreter, you need to create a new thread state.

First you need to create the sub interpreters:

Initialize Python

Py_Initialize();

Initialize Python thread support

Required if you plan to call Python from multiple threads). This call also acquires the GIL.

PyEval_InitThreads();

Save the current thread state

I could have used PyEval_SaveThread(), but one of its side effects is releasing the GIL, which then needs to be reacquired.

PyThreadState* _main = PyThreadState_Get();

Create the sub interpreters

PyThreadState* ts1 = Py_NewInterpreter();PyThreadState* ts2 = Py_NewInterpreter();

Restore the main interpreter thread state

PyThreadState_Swap(_main);

We now have two thread states for the sub interpreters. These thread states are only valid in the thread where they were created. Every thread that wants to use one of the sub interpreters needs to create a thread state for that combination of thread and interpreter.

Using a sub interpreter from a new thread

Here is an example code for using a sub interpreter in a new thread that is not created by the sub interpreter. The new thread must acquire the GIL, create a new thread state for the thread and interpretere combination and make it the current thread state. At the end the reverse must be done to clean up.

void do_stuff_in_thread(PyInterpreterState* interp){    // acquire the GIL    PyEval_AcquireLock();     // create a new thread state for the the sub interpreter interp    PyThreadState* ts = PyThreadState_New(interp);    // make ts the current thread state    PyThreadState_Swap(ts);    // at this point:    // 1. You have the GIL    // 2. You have the right thread state - a new thread state (this thread was not created by python) in the context of interp    // PYTHON WORK HERE    // release ts    PyThreadState_Swap(NULL);    // clear and delete ts    PyThreadState_Clear(ts);    PyThreadState_Delete(ts);    // release the GIL    PyEval_ReleaseLock(); }

Using a sub interpreter from a new thread (post Python 3.3)

The previous do_stuff_in_thread() still works with all current Python versions. However, Python 3.3 deprecated PyEval_AcquireLock()/PyEval_ReleaseLock(), which resulted in a bit of a conundrum.

The only documented way to release the GIL is by calling PyEval_ReleaseThread() or PyEval_SaveThread(), both of which require a thread state, while cleaning and deleting the current thread state requires the GIL to be held. That means that one can either release the GIL or clean up the thread state, but not both.

Fortunately, there is a solution - PyThreadState_DeleteCurrent() deletes the current thread state and then releases the GIL. [This API has only been documented since 3.9, but it existed since Python 2.7 at least]

This modified do_stuff_in_thread() also works with all current Python versions.

void do_stuff_in_thread(PyInterpreterState* interp){    // create a new thread state for the the sub interpreter interp    PyThreadState* ts = PyThreadState_New(interp);    // make it the current thread state and acquire the GIL    PyEval_RestoreThread(ts);    // at this point:    // 1. You have the GIL    // 2. You have the right thread state - a new thread state (this thread was not created by python) in the context of interp    // PYTHON WORK HERE    // clear ts    PyThreadState_Clear(ts);    // delete the current thread state and release the GIL    PyThreadState_DeleteCurrent();}

Now each thread can do the following:

Thread1

do_stuff_in_thread(ts1->interp);

Thread2

do_stuff_in_thread(ts1->interp);

Thread3

do_stuff_in_thread(ts2->interp);

Calling Py_Finalize() destroys all sub interpreters. Alternatively they can be destroyed manually. This needs to be done in the main thread, using the thread states created when creating the sub interpreters. At the end make the main interpreter thread state the current state.

// make ts1 the current thread statePyThreadState_Swap(ts1);// destroy the interpreterPy_EndInterpreter(ts1);// make ts2 the current thread statePyThreadState_Swap(ts2);// destroy the interpreterPy_EndInterpreter(ts2);// restore the main interpreter thread statePyThreadState_Swap(_main);

I hope this make things a bit clearer.

I have a small complete example written in C++ on github, and another also on github (post Python 3.3 variant).


just need to point out a problem in @sterin's answer, in part Using a sub interpreter from a new thread (post Python 3.3)

  • PyThreadState_New must be called when GIL is held
  • PyEval_RestoreThread will acquire the GIL, so it must not be called with GIL held, otherwise a deadlock will show up.

As a result, you need to use PyThreadState_Swap in this case instead of PyEval_RestoreThread

Also, you can verify which interpreter is being used by

int64_t interp_id = PyInterpreterState_GetID(interp);