Многопоточный многопроцессорный API Python API

Я играю с C API для Python, но довольно сложно понять некоторые угловые случаи. Я мог бы протестировать его, но он, похоже, подвержен ошибкам и требует много времени. Поэтому я прихожу сюда, чтобы убедиться, что кто-то уже это сделал.

Вопрос в том, что это правильный способ управления многопоточным с суб-интерпретаторами без прямой связи между потоками и суб-интерпретаторами?

Py_Initialize();
PyEval_InitThreads(); /* <-- needed? */
_main = PyEval_SaveThread(); /* <-- acquire lock? does it matter? */
/* maybe do I not need it? */
i1 = Py_NewInterpreter();
i2 = Py_NewInterpreter();

Я использую мьютекс? Требуется использовать замки? Функция threaded должна выглядеть примерно так: (Нити - это не-python, возможно, потоки POSIX)

Резьба1

_save = PyThreadState_Swap(i1);
  // python work 
PyThreadState_Restore(_save);

Thread2 (почти идентичный)

_save = PyThreadState_Swap(i1);
  // python work 
PyThreadState_Restore(_save);

Thread3 (почти идентичный, но с суб-интерпретатором i2)

_save = PyThreadState_Swap(i2);
  // python work 
PyThreadState_Restore(_save);

Это правильно? Это общий случай, который я хочу достичь? Существуют ли условия гонки?

Спасибо!

Ответ 1

Подинтерпретаторы в Python плохо документированы или даже не поддерживаются. Следующее в меру моего понимания. Кажется, это хорошо работает на практике.

Есть два важных понятия, которые нужно понимать при работе с потоками и субинтерпретаторами в Python. Во-первых, интерпретатор Python не является многопоточным. Он имеет глобальную блокировку интерпретатора (GIL), которую необходимо получить для выполнения практически любой операции с Python (из этого правила есть несколько редких исключений).

Во-вторых, каждая комбинация потока и подчиненного интерпретатора должна иметь свое собственное состояние потока. Интерпретатор создает состояние потока для каждого потока, которым он управляет, но если вы хотите использовать Python из потока, не созданного этим интерпретатором, вам необходимо создать новое состояние потока.

Сначала вам нужно создать подчиненные интерпретаторы:

Инициализировать Python

Py_Initialize();

Инициализация поддержки потока Python

Требуется, если вы планируете вызывать Python из нескольких потоков). Этот звонок также приобретает GIL.

PyEval_InitThreads();

Сохранить текущее состояние потока

Я мог бы использовать PyEval_SaveThread(), но одним из его побочных эффектов является освобождение GIL, который затем необходимо повторно получить.

PyThreadState* _main = PyThreadState_Get();

Создайте подчиненные переводчики

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

Восстановление основного состояния потока интерпретатора

PyThreadState_Swap(_main);

Теперь у нас есть два состояния потока для подчиненных интерпретаторов. Эти состояния потока действительны только в том потоке, в котором они были созданы. Каждый поток, который хочет использовать один из подчиненных интерпретаторов, должен создать состояние потока для этой комбинации потока и интерпретатора.

Использование подчиненного интерпретатора из нового потока

Вот пример кода для использования подчиненного интерпретатора в новом потоке, который не создан подчиненным интерпретатором. Новый поток должен получить GIL, создать новое состояние потока для комбинации потока и interpretere и сделать его текущим состоянием потока. В конце должно быть сделано обратное, чтобы очистить.

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(); 
}

Использование подчиненного интерпретатора из новой темы (пост Python 3.3)

Предыдущий do_stuff_in_thread() прежнему работает со всеми текущими версиями Python. Тем не менее, Python 3.3 устарел PyEval_AcquireLock()/PyEval_ReleaseLock(), что привело к некоторой загадке.

Единственный документированный способ высвобождения GIL - это вызов PyEval_ReleaseThread() или PyEval_SaveThread(), оба из которых требуют состояния потока, в то время как очистка и удаление текущего состояния потока требует сохранения GIL. Это означает, что можно либо освободить GIL, либо очистить состояние потока, но не оба.

К счастью, есть решение - PyThreadState_DeleteCurrent() удаляет текущее состояние потока и затем освобождает GIL. К сожалению, эта функция не задокументирована, поэтому я не уверен на 100%, что это публичный API.

Этот модифицированный do_stuff_in_thread() также работает со всеми текущими версиями Python.

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();
}

Теперь каждый поток может делать следующее:

Резьба1

do_stuff_in_thread(ts1->interp);

Резьба2

do_stuff_in_thread(ts1->interp);

Thread3

do_stuff_in_thread(ts2->interp);

Вызов Py_Finalize() уничтожает все подчиненные интерпретаторы. В качестве альтернативы они могут быть уничтожены вручную. Это должно быть сделано в основном потоке, используя состояния потока, созданные при создании подчиненных интерпретаторов. В конце сделайте основной поток интерпретатора текущим состоянием.

// make ts1 the current thread state
PyThreadState_Swap(ts1);
// destroy the interpreter
Py_EndInterpreter(ts1);

// make ts2 the current thread state
PyThreadState_Swap(ts2);
// destroy the interpreter
Py_EndInterpreter(ts2);

// restore the main interpreter thread state
PyThreadState_Swap(_main);

Надеюсь, это прояснит ситуацию.

У меня есть небольшой полный пример, написанный на C++ на GitHub, а другие также на GitHub (пост Python 3.3 варианта).