PyEval_InitThreads en Python 3: ¿Cómo / cuándo llamarlo? (La saga continúa hasta la náusea)

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 de acompañamiento 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 scope de la pregunta aquí: ¿qué quiero hacer? Bueno … quiero escribir un módulo de extensión de Python en C que:

  1. pthread hilos de trabajo utilizando la API pthread en C
  2. Invoque devoluciones de llamada de Python desde estos subprocesos C

Bien, entonces comencemos con los documentos de Python. Los documentos de Python 3.2 dicen:

void PyEval_InitThreads ()

Inicialice y adquiera el locking de intérprete global. Se debe llamar en el hilo principal antes de crear un segundo hilo o participar en cualquier otra operación de hilo, como PyEval_ReleaseThread (tstate). No es necesario antes de llamar a PyEval_SaveThread () o PyEval_RestoreThread ().

Así que mi entendimiento aquí es que:

  1. Cualquier módulo de extensión C que PyEval_InitThreads() subprocesos debe llamar a PyEval_InitThreads() desde el subproceso principal antes de que se PyEval_InitThreads() otros subprocesos
  2. 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 locking global de intérprete. Está bien, parece bastante sencillo. Entonces, a primera vista , 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 locking 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 locking, el subproceso actual debe haberlo adquirido.

Er … está bien, 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 bash 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. Por lo tanto, 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 a la función interna take_gil(PyThreadState_GET());

Así que PyEval_InitThreads() definitivamente adquiere la 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() solo 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 progtwig 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 forma segura el código Python desde los subprocesos C de los trabajadores?

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 renunciará automáticamente a GIL después de que se hayan ejecutado varias instrucciones (permitiendo que otro subproceso lo adquiera, lo que lo abandonará nuevamente, y así en). Además, cuando Python está a punto de invocar una llamada del sistema de locking, 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 cualquier cosa relacionada 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); 

He visto síntomas similares a los suyos: 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 incorpora 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); 

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 encoding 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 
  1. 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 

Es necesario tener en cuenta que el locking global de intérpretes aún persiste y, a pesar de proporcionar intérpretes individuales a cada subproceso, en lo que respecta a 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 subprocesos únicos para cada subproceso, no podemos ejecutar subprocesos simultáneamente.

Fuentes: Ejecutar un intérprete de Python en el hilo principal y, a cada hilo, podemos dar su propio intérprete secundario

Tutorial de subprocesos múltiples (msdn)

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

Para citar arriba:

La respuesta corta: no debe preocuparse por liberar la 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. locking del 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 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 interlocking más que el uso normal de las extensiones. Cuando ingresamos a nuestra función, teníamos control sobre Python, así 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 era 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).

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.

¿Cuándo se debe llamar a PyEval_InitThreads?

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 }