PyEval_InitThreads в Python 3: как и когда его вызывать? (сага продолжается до тошноты)

По сути, существует массовая путаница/двусмысленность в отношении того, когда именно PyEval_InitThreads() должен вызываться и какие сопутствующие вызовы API необходимы. официальная документация по Python, к сожалению, очень неоднозначна. Уже есть много вопросов по stackoverflow по этой теме, и действительно, Я лично уже задал вопрос, почти идентичный этому один, поэтому я не удивлюсь, если его закроют как дубликат; но учтите, что на этот вопрос, похоже, нет однозначного ответа. (К сожалению, у меня нет Гвидо Ван Россума на быстром наборе.)

Во-первых, давайте определим объем вопроса здесь: что я хочу сделать? Ну... Я хочу написать модуль расширения Python на C, который будет:

  1. Создавать рабочие потоки с помощью pthread API в C
  2. Вызывать обратные вызовы Python из этих потоков C

Итак, давайте начнем с самой документации Python. В документах Python 3.2 сказано:

аннулировать PyEval_InitThreads()

Инициализировать и получить глобальную блокировку интерпретатора. Его следует вызывать в основном потоке перед созданием второго потока или выполнением любых других операций потока, таких как PyEval_ReleaseThread(tstate). Это не требуется перед вызовом PyEval_SaveThread() или PyEval_RestoreThread().

Итак, я понимаю, что вот что:

  1. Любой модуль расширения C, который порождает потоки, должен вызывать PyEval_InitThreads() из основного потока до того, как будут порождены любые другие потоки.
  2. Вызов PyEval_InitThreads блокирует GIL

Итак, здравый смысл подсказывает нам, что любой модуль расширения C, создающий потоки, должен вызвать PyEval_InitThreads(), а затем снять глобальную блокировку интерпретатора. Хорошо, кажется достаточно простым. Итак, на первый взгляд, все, что требуется, это следующий код:

PyEval_InitThreads(); /* initialize threading and acquire GIL */
PyEval_ReleaseLock(); /* Release GIL */

Кажется достаточно простым... но, к сожалению, в документации по Python 3.2 также говорится, что PyEval_ReleaseLock был устарел. Вместо этого мы должны использовать PyEval_SaveThread< /a> для выпуска GIL:

PyThreadState* PyEval_SaveThread()

Снимите глобальную блокировку интерпретатора (если она была создана и включена поддержка потоков) и сбросьте состояние потока в NULL, возвращая предыдущее состояние потока (которое не равно NULL). Если блокировка была создана, текущий поток должен ее получить.

Э... хорошо, я думаю, что модуль расширения C должен сказать:

PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();


Действительно, это именно то, что
этот ответ stackoverflow< /strong> говорит. За исключением тех случаев, когда я действительно пытаюсь сделать это на практике, интерпретатор Python немедленно выдает ошибку сегментации, когда я импортирую модуль расширения. Хороший.


Итак, теперь я отказываюсь от официальной документации Python и обращаюсь к Google. Итак, этот случайный блог утверждает, что все, что вам нужно сделать из модуля расширения, это вызвать PyEval_InitThreads(). Конечно, в документации утверждается, что PyEval_InitThreads() получает GIL, и действительно, быстрая проверка исходного кода для PyEval_InitThreads() в ceval.c показывает, что он действительно вызывает внутреннюю функцию take_gil(PyThreadState_GET());

Итак, PyEval_InitThreads() определенно приобретает GIL. Тогда я бы подумал, что вам абсолютно необходимо как-то освободить GIL после вызова PyEval_InitThreads(). Но как? PyEval_ReleaseLock() устарело, а PyEval_SaveThread() просто необъяснимо seg-faults.

Хорошо... так что, возможно, по какой-то причине, которая в настоящее время находится за пределами моего понимания, модуль расширения C не нуждается в выпуске GIL. Я попробовал это... и, как и ожидалось, как только другой поток попытается получить GIL (используя PyGILState_Ensure), программа зависает из тупика. Так что да... вам действительно нужно освободить GIL после вызова PyEval_InitThreads().

Итак, снова вопрос: как вы выпускаете GIL после вызова PyEval_InitThreads()?

И в более общем плане: что именно должен делать модуль расширения C, чтобы иметь возможность безопасно вызывать код Python из рабочих C-потоков?


person Channel72    schedule 18.03.2013    source источник
comment
связанные: код Python вызывает C библиотека, которая создает потоки ОС, которые в конечном итоге вызывают обратные вызовы Python. См. пример c_extension модуля здесь (его цель — вызвать ошибку в threading, будучи правильным для выявить ошибку в реализации threading. Это не может вызвать ошибку на Python 3).   -  person jfs    schedule 18.03.2013
comment
Вам удалось это решить? У меня точно такая же проблема, и мое приложение c продолжает выдавать ошибки независимо от того, что я делаю.   -  person João Pereira    schedule 13.05.2014


Ответы (8)


Вы правильно поняли: вызов PyEval_InitThreads, среди прочего, получает GIL. В правильно написанном приложении Python/C это не проблема, потому что GIL будет разблокирован вовремя, либо автоматически, либо вручную.

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

Первоначальная версия этого ответа в значительной степени закончилась здесь. Но есть еще одна вещь, которую следует учитывать: сценарий встраивания.

При встраивании Python основной поток часто инициализирует Python и продолжает выполнять другие задачи, не связанные с Python. В этом сценарии нет ничего, что могло бы автоматически освободить GIL, поэтому это должен сделать сам поток. Это никоим образом не относится к вызову, который вызывает PyEval_InitThreads, он ожидается от весь код Python/C, вызванный с помощью полученного GIL.

Например, main() может содержать такой код:

Py_Initialize();
PyEval_InitThreads();

Py_BEGIN_ALLOW_THREADS
... call the non-Python part of the application here ...
Py_END_ALLOW_THREADS

Py_Finalize();

Если ваш код создает потоки вручную, им необходимо получить GIL, прежде чем делать все, связанные с Python, даже такие простые, как Py_INCREF. Для этого используйте следующее :

// Acquire the GIL
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

... call Python code here ...

// Release the GIL. No Python API allowed beyond this point.
PyGILState_Release(gstate);
person user4815162342    schedule 18.03.2013
comment
PyGILState_Ensure()/PyGILState_Release() следует использовать для вызова Python из потока C вместо PyEval_{Save,Restore}Thread() См. Состояние потока и глобальная блокировка интерпретатора - person jfs; 18.03.2013
comment
@ J.F.Sebastian Согласен, теперь я отредактировал ответ, чтобы рекламировать правильный API. - person user4815162342; 18.03.2013
comment
Но, как я уже упоминал в вопросе, если основной поток удерживает GIL (что будет после вызова PyEval_InitThreads(), то когда рабочий поток вызывает PyGILState_Ensure, он переходит в тупик. - person Channel72; 18.03.2013
comment
@Channel72 Почему это зашло в тупик? Он будет ждать столько, сколько необходимо для приобретения GIL. Основной поток должен рано или поздно отказаться от GIL — либо он введет блокирующий системный вызов, освобождающий GIL, либо интерпретатор освободит его автоматически после выполнения ряда инструкций байт-кода. - person user4815162342; 18.03.2013
comment
@user4815162342 user4815162342 - Это приведет к тупику, если поток, который вызывает PyEval_InitThreads, на самом деле ничего не делает с Python. Это произошло в каком-то вызове Init, поэтому в этот момент не обязательно было вызывать Python. Значит, этот поток просто занимается своими делами и игнорирует блокировку GIL до конца выполнения программы? - person DougN; 17.04.2013
comment
@DougN Как это ничего не может сделать с Python? Нельзя просто вызвать PyEval_InitThreads наугад. Фактически, ответ явно рекомендует вызывать PyEval_InitThreads в расширении <module>init. Если это будет соблюдено, функция инициализации вернется к вызывающему объекту Python, который инициировал импорт. Python с радостью продолжит свой путь, в конечном итоге выпустив GIL, как подробно описано в ответе. - person user4815162342; 18.04.2013
comment
Для меня это все еще приводит к сбою на стороне C++. Я получаю нарушение прав доступа... - person SkyWalker; 20.12.2016
comment
@user4815162342 Относительно вашего вопроса, почему это может зайти в тупик?: Насколько я понимаю, PyGILState_Ensure(), который пытается получить GIL, БУДЕТ тупиком (в частности, застрянет в бесконечном цикле внутри функции take_gil() в исходном коде Python), если GIL не извлекаемый. Чтобы его можно было восстановить, после вызова PyEval_InitThreads() код, вызывающий эту функцию, также должен вызывать PyEval_SaveThread(). (Кроме того, PyEval_RestoreThread() снова сделает GIL недоступным для извлечения PyGILState_Ensure().) - person andreasdr; 04.01.2018
comment
@andreasdr Но почему может возникнуть такая ситуация, кроме как столкнуться с вредоносным или заведомо неправильным кодом? Код расширения, вызывающий PyEval_InitThreads, очевидно, поддерживает Python. Таким образом, его поток будет либо продолжать вызывать Python, что приведет к выпуску GIL раньше или позже (в цикле интерпретатора или вызовом блокирующего ввода-вывода из Python); или он вызовет код C, и в этом случае его ответственность состоит в освобождении GIL перед этим (и последующем повторном его получении). Честно говоря, я не понимаю, в чем заключается вся путаница - когда я это сделаю, я изменю ответ, чтобы решить эту проблему. - person user4815162342; 04.01.2018
comment
Я имею дело с другим случаем, чем OP (см. этот ответ или мой SO-вопрос): я инициализирую интерпретатор из кода C++ (т.е. не из расширения Python), который больше не будет взаимодействовать с API Python, за исключением завершения интерпретатора. . Поэтому, если я не выпущу GIL явно, он никогда не будет выпущен, и любой вызов PyGILState_Ensure() из потока, начатого из кода C++, застрянет в бесконечном цикле, пытаясь получить GIL. - person andreasdr; 05.01.2018
comment
@andreasdr Думаю, теперь я понимаю путаницу. Я изменил ответ, чтобы решить эту проблему. - person user4815162342; 05.01.2018

Я видел симптомы, похожие на ваши: взаимоблокировки, если я вызываю только PyEval_InitThreads(), потому что мой основной поток больше никогда ничего не вызывает из Python, и segfaults, если я безоговорочно вызываю что-то вроде PyEval_SaveThread(). Симптомы зависят от версии Python и от ситуации: я разрабатываю подключаемый модуль, который встраивает Python для библиотеки, которую можно загрузить как часть расширения Python. Поэтому код должен работать независимо от того, загружен ли он Python как основной.

Следующее работало как с python2.7, так и с python3.4, а также с моей библиотекой, работающей в Python и вне Python. В моей подпрограмме инициализации плагина, которая выполняется в основном потоке, я запускаю:

  Py_InitializeEx(0);
  if (!PyEval_ThreadsInitialized()) {
    PyEval_InitThreads();
    PyThreadState* mainPyThread = PyEval_SaveThread();
  }

(mainPyThread на самом деле является некоторой статической переменной, но я не думаю, что это имеет значение, поскольку мне больше никогда не понадобится ее использовать).

Затем я создаю потоки, используя pthreads, и в каждой функции, которой требуется доступ к Python API, я использую:

  PyGILState_STATE gstate;
  gstate = PyGILState_Ensure();
  // Python C API calls
  PyGILState_Release(gstate);
person thibaut    schedule 17.06.2015
comment
Я использую Python 2.6, и если я запускаю это решение в основном потоке, оно зависает в основном потоке. - person SkyWalker; 20.12.2016
comment
Я предполагаю, что правильный путь состоит в том, чтобы в конечном итоге снова получить GIL в основном потоке через PyEval_RestoreThread(mainPyThread), где-то перед Py_FinalizeEx(). Прежде чем сделать это, обязательно подождите, пока все потоки закончат свое выполнение, иначе любой вызов PyGILState_Ensure() в любом потоке заблокируется (бесконечный цикл внутри функции take_gil() в исходном коде Python), потому что GIL теперь снова недоступен. - person andreasdr; 04.01.2018
comment
@andreasdr Действительно, если поток, который вызывает PyEval_InitThreads, действительно не делает ничего, связанного с Python, до конца своего существования, тогда правильно немедленно вызывать PyEval_SaveThread и вызывать PyEval_RestoreThread только при завершении работы. У меня есть последнее замечание — возможно, вы неправильно используете термин «тупик», что означает ожидание условия, которое никогда не может быть выполнено. take_gil на самом деле удастся получить GIL, как только он станет доступен. - person user4815162342; 04.01.2018
comment
@ user4815162342 Я согласен, я неправильно использую этот термин; поток застревает в цикле, который постоянно пытается получить GIL, поэтому он вполне может в конечном итоге преуспеть, если что-то изменится в другом потоке, чтобы сделать его доступным. Но что вы имеете в виду, когда говорите что-то о Python? Разве выпуск GIL не должен быть сделан явным образом? Или вы имеете в виду, что любое взаимодействие с Python API влияет на GIL? - person andreasdr; 04.01.2018
comment
@andreasdr Да, точно. Всякий раз, когда вы каким-либо образом взаимодействуете с интерпретатором Python, вы должны удерживать GIL, и Python будет освобождать его для вас двумя способами: 1) через равные промежутки времени по мере выполнения кода Python (это позволяет другим потокам которые требуют запуска GIL), и 2) вокруг вызова блокирующей или потенциально блокирующей функции (например, что-либо, связанное с вводом-выводом). Только когда вы пишете свой собственный код C, не имеющий ничего общего с Python, вам нужно вручную выпустить GIL — и даже тогда GIL необходимо повторно получить, прежде чем снова делать что-либо с Python в этом потоке. - person user4815162342; 04.01.2018
comment
@ user4815162342 Последний пример, это именно то, что я делаю :). Спасибо за информацию, картина стала намного яснее! - person andreasdr; 05.01.2018

Существует два метода многопоточности при выполнении C/Python API.

1. Выполнение разных потоков с одним и тем же интерпретатором. Мы можем выполнить интерпретатор Python и использовать один и тот же интерпретатор в разных потоках.

Кодирование будет следующим.

main(){     
//initialize Python
Py_Initialize();
PyRun_SimpleString("from time import time,ctime\n"
    "print 'In Main, Today is',ctime(time())\n");

//to Initialize and acquire the global interpreter lock
PyEval_InitThreads();

//release the lock  
PyThreadState *_save;
_save = PyEval_SaveThread();

// Create threads.
for (int i = 0; i<MAX_THREADS; i++)
{   
    hThreadArray[i] = CreateThread
    //(...
        MyThreadFunction,       // thread function name
    //...)

} // End of main thread creation loop.

// Wait until all threads have terminated.
//...
//Close all thread handles and free memory allocations.
//...

//end python here
//but need to check for GIL here too
PyEval_RestoreThread(_save);
Py_Finalize();
return 0;
}

//the thread function

DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
//non Pythonic activity
//...

//check for the state of Python GIL
PyGILState_STATE gilState;
gilState = PyGILState_Ensure();
//execute Python here
PyRun_SimpleString("from time import time,ctime\n"
    "print 'In Thread Today is',ctime(time())\n");
//release the GIL           
PyGILState_Release(gilState);   

//other non Pythonic activity
//...
return 0;
}
  1. Другой метод заключается в том, что мы можем выполнять интерпретатор Python в основном потоке, и каждому потоку мы можем дать свой собственный интерпретатор. Таким образом, каждый поток работает со своими отдельными независимыми версиями всех импортированных модулей, включая основные модули — встроенные модули, __main__ и sys.

Код выглядит следующим образом

int main()
{

// Initialize the main interpreter
Py_Initialize();
// Initialize and acquire the global interpreter lock
PyEval_InitThreads();
// Release the lock     
PyThreadState *_save;
_save = PyEval_SaveThread();


// create threads
for (int i = 0; i<MAX_THREADS; i++)
{

    // Create the thread to begin execution on its own.

    hThreadArray[i] = CreateThread
    //(...

        MyThreadFunction,       // thread function name
    //...);   // returns the thread identifier 

} // End of main thread creation loop.

  // Wait until all threads have terminated.
WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);

// Close all thread handles and free memory allocations.
// ...


//end python here
//but need to check for GIL here too
//re capture the lock
PyEval_RestoreThread(_save);
//end python interpreter
Py_Finalize();
return 0;
}

//the thread functions
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
// Non Pythonic activity
// ...

//create a new interpreter
PyEval_AcquireLock(); // acquire lock on the GIL
PyThreadState* pThreadState = Py_NewInterpreter();
assert(pThreadState != NULL); // check for failure
PyEval_ReleaseThread(pThreadState); // release the GIL


// switch in current interpreter
PyEval_AcquireThread(pThreadState);

//execute python code
PyRun_SimpleString("from time import time,ctime\n" "print\n"
    "print 'Today is',ctime(time())\n");

// release current interpreter
PyEval_ReleaseThread(pThreadState);

//now to end the interpreter
PyEval_AcquireThread(pThreadState); // lock the GIL
Py_EndInterpreter(pThreadState);
PyEval_ReleaseLock(); // release the GIL

// Other non Pythonic activity
return 0;
}

Необходимо отметить, что глобальная блокировка интерпретатора по-прежнему сохраняется, и, несмотря на предоставление отдельных интерпретаторов каждому потоку, когда дело доходит до выполнения Python, мы по-прежнему можем выполнять только один поток за раз. GIL является УНИКАЛЬНЫМ для PROCESS, поэтому, несмотря на предоставление уникального подинтерпретатора для каждого потока, мы не можем иметь одновременное выполнение потоков

Источники: Выполнение интерпретатора Python в основной поток, и каждому потоку мы можем дать свой собственный интерпретатор

Учебное пособие по многопоточности (msdn)

person ashhadul islam    schedule 08.03.2017

Чтобы процитировать выше:

Короткий ответ: вам не нужно выпускать GIL после вызова PyEval_InitThreads...

Теперь более развернутый ответ:

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

Если потоки НЕ были инициализированы, то мы знаем, что нет GIL (нет потоков == нет необходимости в блокировке), и, следовательно, «небезопасно вызывать эту функцию, когда неизвестно, какой поток (если есть) в настоящее время имеет глобальный блокировка интерпретатора" не применяется.

if (!PyEval_ThreadsInitialized())
{
    PyEval_InitThreads();
}

После вызова PyEval_InitThreads() создается GIL и назначается... нашему потоку, который в настоящее время выполняет код Python. Так что все хорошо.

Теперь, что касается наших собственных запущенных рабочих «C»-потоков, им нужно будет запрашивать GIL перед запуском соответствующего кода: поэтому их общая методология выглядит следующим образом:

// Do only non-Python things up to this point
PyGILState_STATE state = PyGILState_Ensure();
// Do Python-things here, like PyRun_SimpleString(...)
PyGILState_Release(state);
// ... and now back to doing only non-Python things

Нам не нужно беспокоиться о тупиковой ситуации больше, чем об обычном использовании расширений. Когда мы вошли в нашу функцию, у нас был контроль над Python, поэтому либо мы не использовали потоки (таким образом, без GIL), либо GIL уже был нам назначен. Когда мы вернем управление среде выполнения Python, выйдя из нашей функции, обычный цикл обработки проверит GIL и передаст управление другим запрашивающим объектам, включая наши рабочие потоки через PyGILState_Ensure().

Все это читатель наверняка уже знает. Однако «доказательство в пудинге». Я опубликовал очень минимально документированный пример, который я написал сегодня, чтобы узнать для себя, что на самом деле было поведением, и что все работает правильно. Пример исходного кода на GitHub

На этом примере я узнал несколько вещей, в том числе интеграцию CMake с разработкой Python, интеграцию SWIG с обоими вышеперечисленными и поведение Python с расширениями и потоками. Тем не менее, ядро ​​примера позволяет вам:

  • Загрузите модуль -- 'импорт раздражает'
  • Загрузить ноль или более рабочих потоков, которые выполняют функции Python — 'annoy.annoy(n)'
  • Очистить все рабочие потоки -- 'annon.annoy(0)'
  • Обеспечить очистку потока (в Linux) при выходе из приложения

... и все это без каких-либо сбоев или ошибок. По крайней мере, в моей системе (Ubuntu Linux с GCC).

person Joe Marley    schedule 16.09.2015

Предложение вызвать PyEval_SaveThread работает

PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();

Однако, чтобы предотвратить сбой при импорте модуля, убедитесь, что API-интерфейсы Python для импорта защищены с помощью

PyGILState_Ensure и PyGILState_Release

e.g.

PyGILState_STATE gstate = PyGILState_Ensure();
PyObject *pyModule_p = PyImport_Import(pyModuleName_p);
PyGILState_Release(gstate);
person Sean    schedule 22.01.2014

Я тоже чувствую замешательство в этом вопросе. Следующий код работает по совпадению.

Py_InitializeEx(0);
    if (!PyEval_ThreadsInitialized()) {
    PyEval_InitThreads();
    PyThreadState* mainPyThread = PyEval_SaveThread();
}

Мой основной поток выполняет некоторую начальную работу во время выполнения Python и создает другой поток для обработки задач. И у меня есть лучший обходной путь для этого. В основной теме:

if (!PyEval_ThreadsInitialized()){
    PyEval_InitThreads();
}
//other codes
while(alive) {
    Py_BEGIN_ALLOW_THREADS
    sleep or other block code
    Py_END_ALLOW_THREADS
}
person chenju    schedule 12.11.2016

Вам не нужно вызывать это в своих модулях extension. Это для инициализации интерпретатора, которая уже была выполнена, если ваш модуль расширения C-API импортируется. Этот интерфейс предназначен для встраивания приложений.

Когда должен вызываться PyEval_InitThreads?

person jwp    schedule 18.03.2013
comment
нет, вам нужно вызвать PyEval_InitThreads, если вы планируете выполнять обратные вызовы Python из нескольких потоков, отличных от Python (как ответ, который вы связали говорит) - person jfs; 18.03.2013
comment
Посмотрев немного на источник, может показаться, что вы технически правы. Однако в документации нет такого предложения по использованию, поэтому я бы не стал использовать интерфейс таким образом. Фактически, docs.python.org/3.3/c-api/init .html#PyEval_InitThreads указывает, что последующие вызовы не выполняются. - person jwp; 18.03.2013
comment
Хорошо, я думаю, что модули расширения C-API, требующие потоков Python, захотят вызвать это, чтобы убедиться, что gil инициализирован. Лично, если бы приложение для встраивания не инициализировало потоки Python, я бы дважды подумал о запуске потоков. - person jwp; 18.03.2013

Эта функция устарела в Python 3.9: https://docs.python.org/3/c-api/init.html#c.PyEval_InitThreads

Начиная с Python 3.7 функция уже вызывается Py_Initialize().

Начиная с Python 3.9 функция ничего не делает.

В Python 3.11 он будет удален.

person alkino    schedule 09.12.2020