PyEval_InitThreads en Python 3: ¿Cómo/cuándo llamarlo?(La saga continúa hasta la saciedad).
python-3.x python-c-api (7)
Básicamente, parece haber una confusión / ambigüedad masiva sobre cuándo se supone que se debe llamar exactamente PyEval_InitThreads()
, y qué llamadas de API son necesarias. La documentación oficial de Python desafortunadamente es muy ambigua. Ya hay muchas preguntas sobre stackoverflow con respecto a este tema, y de hecho, personalmente ya hice una pregunta casi idéntica a esta, por lo que no me sorprendería particularmente si se cierra como un duplicado; pero considere que no parece haber una respuesta definitiva a esta pregunta. (Lamentablemente, no tengo a Guido Van Rossum en la marcación rápida).
En primer lugar, definamos el alcance de la pregunta aquí: ¿qué quiero hacer? Bueno ... quiero escribir un módulo de extensión de Python en C que:
-
pthread
hilos de trabajo usando la APIpthread
en C - Invoque devoluciones de llamada de Python desde estos subprocesos C
Bien, entonces comencemos con los documentos de Python. Los PyEval_InitThreads() dicen:
void PyEval_InitThreads ()
Inicialice y adquiera el bloqueo de intérprete global. Se debe llamar en el subproceso principal antes de crear un segundo subproceso o participar en cualquier otra operación de subproceso, como PyEval_ReleaseThread (tstate). No es necesario antes de llamar a PyEval_SaveThread () o PyEval_RestoreThread ().
Así que mi entendimiento aquí es que:
- Cualquier módulo de extensión C que
PyEval_InitThreads()
subprocesos debe llamar aPyEval_InitThreads()
desde el subproceso principal antes de que sePyEval_InitThreads()
otros subprocesos - Llamar a
PyEval_InitThreads
bloquea la GIL
El sentido común nos diría que cualquier módulo de extensión C que cree subprocesos debe llamar a PyEval_InitThreads()
y luego liberar el bloqueo global de intérprete. Está bien, parece bastante sencillo. Así que prima facie , todo lo que se requiere sería el siguiente código:
PyEval_InitThreads(); /* initialize threading and acquire GIL */
PyEval_ReleaseLock(); /* Release GIL */
Parece bastante fácil ... pero desafortunadamente, los documentos de Python 3.2 también dicen que PyEval_ReleaseLock
ha sido desaprobado . En lugar de eso, se supone que PyEval_SaveThread
usar PyEval_SaveThread
para liberar la GIL:
PyThreadState * PyEval_SaveThread ()
Libere el bloqueo global del intérprete (si se ha creado y el soporte de subprocesos está habilitado) y restablezca el estado del subproceso a NULL, devolviendo el estado del subproceso anterior (que no es NULL). Si se ha creado el bloqueo, el subproceso actual debe haberlo adquirido.
Er ... bueno, así que supongo que un módulo de extensión C necesita decir:
PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();
De hecho, esto es exactamente lo que dice esta respuesta de stackoverflow . Excepto cuando realmente intento esto en la práctica, el intérprete de Python inmediatamente falla cuando importo el módulo de extensión. Bonito.
Bien, ahora renuncio a la documentación oficial de Python y me dirijo a Google. Entonces, este blog aleatorio afirma que todo lo que necesita hacer desde un módulo de extensión es llamar a PyEval_InitThreads()
. Por supuesto, la documentación afirma que PyEval_InitThreads()
adquiere el GIL, y de hecho, una inspección rápida del código fuente de PyEval_InitThreads()
en ceval.c
revela que sí llama la función interna take_gil(PyThreadState_GET());
Así que PyEval_InitThreads()
definitivamente adquiere el GIL. PyEval_InitThreads()
entonces que sería absolutamente necesario liberar la GIL después de llamar a PyEval_InitThreads()
. ¿Pero cómo? PyEval_ReleaseLock()
está en desuso, y PyEval_SaveThread()
simplemente inexplicablemente genera fallas.
Bueno ... tal vez por alguna razón que actualmente no puedo entender, un módulo de extensión C no necesita liberar la GIL. Intenté eso ... y, como era de esperar, tan pronto como otro subproceso intenta adquirir el GIL (usando PyGILState_Ensure ), el programa se cuelga de un punto muerto. Entonces sí ... realmente necesitas liberar la GIL después de llamar a PyEval_InitThreads()
.
Entonces, nuevamente, la pregunta es: ¿cómo se libera la GIL después de llamar a PyEval_InitThreads()
?
Y de manera más general: ¿qué debe hacer exactamente un módulo de extensión C para poder invocar de manera segura el código Python desde los subprocesos C de los trabajadores?
Hay dos métodos de subprocesamiento múltiple al ejecutar la API de C / Python.
1. Ejecución de diferentes subprocesos con el mismo intérprete: podemos ejecutar un intérprete de Python y compartir el mismo intérprete en los diferentes subprocesos.
La codificación será la siguiente.
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;
}
- Otro método es que podemos ejecutar un intérprete de Python en el subproceso principal y, a cada subproceso, podemos otorgar su propio subproctor. Por lo tanto, cada hilo se ejecuta con sus propias versiones independientes e independientes de todos los módulos importados, incluidos los módulos fundamentales: builtins, __main__ y sys.
El código es el siguiente
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;
}
Es necesario tener en cuenta que el bloqueo global de intérpretes aún persiste y, a pesar de dar intérpretes individuales a cada subproceso, cuando se trata de la ejecución de Python, podemos ejecutar solo un subproceso a la vez. GIL es ÚNICO PARA PROCESAR , por lo que, a pesar de proporcionar un subprocesador único para cada subproceso, no podemos ejecutar subprocesos simultáneamente.
He visto síntomas similares a los tuyos: puntos muertos si solo llamo a PyEval_InitThreads (), porque mi hilo principal nunca vuelve a llamar a Python, y sigue si aparece incondicionalmente a algo como PyEval_SaveThread (). Los síntomas dependen de la versión de Python y de la situación: estoy desarrollando un complemento que incrusta Python para una biblioteca que se puede cargar como parte de una extensión de Python. Por lo tanto, el código debe ejecutarse independientemente de si Python lo carga como principal.
Lo siguiente funcionó tanto con python2.7 como con python3.4, y con mi biblioteca ejecutándose dentro de Python y fuera de Python. En mi rutina de inicio de plug-in, que se ejecuta en el hilo principal, ejecuto:
Py_InitializeEx(0);
if (!PyEval_ThreadsInitialized()) {
PyEval_InitThreads();
PyThreadState* mainPyThread = PyEval_SaveThread();
}
(mainPyThread es en realidad una variable estática, pero no creo que eso importe ya que nunca más necesito volver a usarla).
Luego creo subprocesos usando pthreads, y en cada función que necesita acceder a la API de Python, uso:
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
// Python C API calls
PyGILState_Release(gstate);
La sugerencia de llamar a PyEval_SaveThread funciona.
PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();
Sin embargo, para evitar que se bloquee cuando se importa el módulo, asegúrese de que las API de Python para importar estén protegidas usando
PyGILState_Ensure y PyGILState_Release
p.ej
PyGILState_STATE gstate = PyGILState_Ensure();
PyObject *pyModule_p = PyImport_Import(pyModuleName_p);
PyGILState_Release(gstate);
Me siento confuso sobre este tema también. El siguiente código funciona por coincidencia.
Py_InitializeEx(0);
if (!PyEval_ThreadsInitialized()) {
PyEval_InitThreads();
PyThreadState* mainPyThread = PyEval_SaveThread();
}
Mi hilo principal hace un trabajo inicial de Python Runtime y crea otro pthread para manejar las tareas. Y tengo una mejor solución para esto. En el hilo principal:
if (!PyEval_ThreadsInitialized()){
PyEval_InitThreads();
}
//other codes
while(alive) {
Py_BEGIN_ALLOW_THREADS
sleep or other block code
Py_END_ALLOW_THREADS
}
No necesita llamar a eso en sus módulos de extensión . Eso es para inicializar el intérprete que ya se ha hecho si se está importando su módulo de extensión C-API. Esta interfaz se utilizará para incrustar aplicaciones.
Para citar arriba:
La respuesta corta: no debe preocuparse por liberar GIL después de llamar a PyEval_InitThreads ...
Ahora, para una respuesta más larga:
Estoy limitando mi respuesta para que sea sobre las extensiones de Python (en lugar de incrustar Python). Si solo estamos extendiendo Python, cualquier punto de entrada en su módulo es de Python. Esto, por definición, significa que no tenemos que preocuparnos por llamar a una función desde un contexto que no sea de Python, lo que simplifica un poco las cosas.
Si los subprocesos NO se han inicializado, entonces sabemos que no hay GIL (no hay subprocesos == no hay necesidad de bloquear), y por lo tanto, "No es seguro llamar a esta función cuando no se sabe qué subproceso (si existe) tiene actualmente el global. bloqueo de intérprete "no se aplica.
if (!PyEval_ThreadsInitialized())
{
PyEval_InitThreads();
}
Después de llamar a PyEval_InitThreads (), se crea una GIL y se asigna ... a nuestro hilo, que es el hilo que actualmente ejecuta el código de Python. Así que todo está bien.
Ahora, en lo que respecta a nuestros propios hilos de trabajador "C" lanzados, deberán solicitar el GIL antes de ejecutar el código relevante: su metodología común es la siguiente:
// 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
No tenemos que preocuparnos por el interbloqueo más que el uso normal de las extensiones. Cuando ingresamos a nuestra función, teníamos control sobre Python, por lo que o no estábamos usando subprocesos (por lo tanto, no GIL), o el GIL ya estaba asignado a nosotros. Cuando le devolvemos el control al tiempo de ejecución de Python al salir de nuestra función, el ciclo de procesamiento normal verificará el GIL y el control manual según corresponda a otros objetos solicitantes: incluidos nuestros subprocesos de trabajo a través de PyGILState_Ensure ().
Todo esto el lector probablemente ya lo sabe. Sin embargo, la "prueba está en el pudín". He publicado un ejemplo muy poco documentado que escribí hoy para aprender por mí mismo cuál fue realmente el comportamiento y que las cosas funcionan correctamente. Código fuente de muestra en GitHub
Aprendí varias cosas con el ejemplo, incluida la integración de CMake con el desarrollo de Python, la integración de SWIG con los dos anteriores y los comportamientos de Python con extensiones y subprocesos. Aún así, el núcleo del ejemplo le permite:
- Cargar el módulo - ''import molestar''
- Cargue cero o más subprocesos de trabajo que hagan cosas de Python - ''annoy.annoy (n)''
- Borrar cualquier subproceso de trabajo - ''annon.annoy (0)''
- Proporcionar limpieza de subprocesos (en Linux) al salir de la aplicación
... y todo esto sin ningún tipo de choques o seguridades. Al menos en mi sistema (Ubuntu Linux w / GCC).
Tu entendimiento es correcto: invocar PyEval_InitThreads
, entre otras cosas, adquiere el GIL. En una aplicación Python / C correctamente escrita, esto no es un problema porque la GIL se desbloqueará a tiempo, de forma automática o manual.
Si el hilo principal continúa ejecutando el código de Python, no hay nada especial que hacer, porque el intérprete de Python abandonará automáticamente el GIL después de que se hayan ejecutado varias instrucciones (permitiendo que otro hilo lo adquiera, lo que lo abandonará de nuevo, y así) en). Además, cuando Python está a punto de invocar una llamada del sistema de bloqueo, por ejemplo, para leer desde la red o escribir en un archivo, liberará el GIL alrededor de la llamada.
La versión original de esta respuesta prácticamente terminó aquí. Pero hay una cosa más a tener en cuenta: el escenario de incrustación .
Al incrustar Python, el hilo principal a menudo inicializa Python y continúa ejecutando otras tareas no relacionadas con Python. En ese escenario, no hay nada que libere automáticamente la GIL, por lo que esto debe hacerlo el propio hilo. Eso no es de ninguna manera específico a la llamada que llama a PyEval_InitThreads
, se espera de todo el código de Python / C invocado con la GIL adquirida.
Por ejemplo, el main()
podría contener código como este:
Py_Initialize();
PyEval_InitThreads();
Py_BEGIN_ALLOW_THREADS
... call the non-Python part of the application here ...
Py_END_ALLOW_THREADS
Py_Finalize();
Si su código crea subprocesos manualmente, deben adquirir el GIL antes de hacer algo relacionado con Python, incluso tan simple como Py_INCREF
. Para hacerlo, usa lo siguiente :
// 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);