Подтвердить что ты не робот

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

В принципе, кажется, что существует массивная путаница/неопределенность, когда именно PyEval_InitThreads() должен быть вызван, и какие сопутствующие вызовы API необходимы. официальная документация Python, к сожалению, очень неоднозначна. Уже есть qaru.site/info/308127/... по этой теме, и действительно, я уже лично задал вопрос почти идентичный к этому, поэтому я не буду особенно удивлен, если он будет закрыт как дубликат; но считают, что, по-видимому, нет окончательного ответа на этот вопрос. (К сожалению, у меня нет Guido Van Rossum на скоростном наборе.)

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

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

Хорошо, давайте начнем с самих документов Python. Python 3.2 docs говорят:

void PyEval_InitThreads()

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

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

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

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

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

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

PyThreadState * PyEval_SaveThread()

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

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

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


В самом деле, это именно то, что говорит qaru.site/info/308127/..... За исключением случаев, когда я на самом деле пытаюсь это на практике, интерпретатор Python немедленно seg-faults, когда я импортирую модуль расширения.   Ницца.


Хорошо, теперь я отказываюсь от официальной документации 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-потоков?

4b9b3361

Ответ 1

Ваше понимание верное: вызов 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);

Ответ 2

Я видел похожие симптомы: deadlocks, если я вызываю только 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, и в каждой функции, которая должна получить доступ к API Python, я использую:

  PyGILState_STATE gstate;
  gstate = PyGILState_Ensure();
  // Python C API calls
  PyGILState_Release(gstate);

Ответ 3

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

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

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

PyGILState_Ensure и PyGILState_Release

например.

PyGILState_STATE gstate = PyGILState_Ensure();
PyObject *pyModule_p = PyImport_Import(pyModuleName_p);
PyGILState_Release(gstate);

Ответ 4

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

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

Теперь, для более длительного ответа:

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

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

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

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

Теперь, что касается наших собственных рабочих "C" -threads, им нужно будет запросить 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 с расширениями и потоками. Тем не менее, ядро ​​примера позволяет вам:

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

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

Ответ 5

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

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

Необходимо отметить, что блокировка Global Interpreter Lock по-прежнему сохраняется и, несмотря на предоставление отдельным интерпретаторам каждого потока, когда дело доходит до выполнения python, мы можем выполнить только один поток за раз. GIL UNIQUE - ПРОЦЕСС, поэтому, несмотря на предоставление уникального суб-интерпретатора для каждого потока, мы не можем выполнять одновременное выполнение потоков

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

Многопоточный учебник (msdn)

Ответ 6

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

Когда вызывается PyEval_InitThreads?

Ответ 7

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

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

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

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