Subprocesos en una aplicación PyQt: ¿Utiliza subprocesos Qt o subprocesos Python?

116

Estoy escribiendo una aplicación GUI que recupera datos con regularidad a través de una conexión web. Dado que esta recuperación lleva un tiempo, esto hace que la interfaz de usuario no responda durante el proceso de recuperación (no se puede dividir en partes más pequeñas). Es por eso que me gustaría subcontratar la conexión web a un hilo de trabajo separado.

[Sí, lo sé, ahora tengo dos problemas ].

De todos modos, la aplicación usa PyQt4, así que me gustaría saber cuál es la mejor opción: ¿Usar los hilos de Qt o usar el threadingmódulo Python ? ¿Cuáles son las ventajas / desventajas de cada uno? ¿O tienes una sugerencia totalmente diferente?

Editar (recompensa): si bien la solución en mi caso particular probablemente será usar una solicitud de red sin bloqueo como sugirieron Jeff Ober y Lukáš Lalinský (así que básicamente dejar los problemas de concurrencia a la implementación de la red), todavía me gustaría más respuesta en profundidad a la pregunta general:

¿Cuáles son las ventajas y desventajas de usar subprocesos de PyQt4 (es decir, Qt) sobre subprocesos nativos de Python (del threadingmódulo)?


Edición 2: Gracias a todos por sus respuestas. Aunque no hay un acuerdo del 100%, parece haber un consenso generalizado de que la respuesta es "usar Qt", ya que la ventaja de eso es la integración con el resto de la biblioteca, sin causar desventajas reales.

Para cualquiera que busque elegir entre las dos implementaciones de subprocesos, les recomiendo que lean todas las respuestas proporcionadas aquí, incluido el hilo de la lista de correo de PyQt al que abbot se vincula.

Había varias respuestas que consideré para la recompensa; al final elegí abad's como referencia externa muy relevante; sin embargo, estuvo cerca.

Gracias de nuevo.

balpha
fuente

Respuestas:

106

Esto se discutió no hace mucho en la lista de correo de PyQt. Citando los comentarios de Giovanni Bajo sobre el tema:

Es casi lo mismo. La principal diferencia es que QThreads se integra mejor con Qt (señales / ranuras asíncronas, bucle de eventos, etc.). Además, no puede usar Qt desde un hilo de Python (por ejemplo, no puede publicar un evento en el hilo principal a través de QApplication.postEvent): necesita un QThread para que eso funcione.

Una regla general podría ser usar QThreads si va a interactuar de alguna manera con Qt, y usar Python en caso contrario.

Y un comentario anterior sobre este tema del autor de PyQt: "ambos son envoltorios de las mismas implementaciones de subprocesos nativos". Y ambas implementaciones usan GIL de la misma manera.

abad
fuente
2
Buena respuesta, pero creo que debería usar el botón blockquote para mostrar claramente dónde se encuentra, de hecho, sin resumir pero citando a Giovanni Bajo de la lista de correo :)
c089
2
Me pregunto por qué no puede publicar eventos en el hilo principal a través de QApplication.postEvent () y necesita un QThread para eso. Creo que he visto gente haciéndolo y funcionó.
Trilarion
1
He llamado QCoreApplication.postEventdesde un hilo de Python a una velocidad de 100 veces por segundo, en una aplicación que se ejecuta en varias plataformas y ha sido probada durante miles de horas. Nunca he visto ningún problema por eso. Creo que está bien siempre que el objeto de destino esté ubicado en MainThread o QThread. También lo envolví en una bonita biblioteca, vea qtutils .
three_pineapples
2
Dada la naturaleza altamente votada de esta pregunta y respuesta, creo que vale la pena señalar una respuesta SO reciente de ekhumoro que explica las condiciones en las que es seguro usar ciertos métodos Qt de subprocesos de Python. Esto concuerda con el comportamiento observado que hemos visto yo y @Trilarion.
three_pineapples
33

Los subprocesos de Python serán más simples y seguros, y dado que son para una aplicación basada en E / S, pueden omitir el GIL. Dicho esto, ¿ha considerado E / S sin bloqueo utilizando sockets / select Twisted o sin bloqueo?

EDITAR: más sobre hilos

Hilos de Python

Los hilos de Python son hilos del sistema. Sin embargo, Python utiliza un bloqueo de intérprete global (GIL) para garantizar que el intérprete solo ejecute un bloque de cierto tamaño de instrucciones de código de bytes a la vez. Afortunadamente, Python libera el GIL durante las operaciones de entrada / salida, lo que hace que los subprocesos sean útiles para simular E / S sin bloqueo.

Advertencia importante: esto puede ser engañoso, ya que el número de instrucciones de código de bytes no se corresponde con el número de líneas de un programa. Incluso una sola asignación puede no ser atómica en Python, por lo que es necesario un bloqueo mutex para cualquier bloque de código que deba ejecutarse atómicamente, incluso con GIL.

Hilos QT

Cuando Python cede el control a un módulo compilado de terceros, libera el GIL. Es responsabilidad del módulo garantizar la atomicidad cuando sea necesario. Cuando se devuelve el control, Python usará el GIL. Esto puede hacer que el uso de bibliotecas de terceros junto con subprocesos sea confuso. Es aún más difícil usar una biblioteca de subprocesos externa porque agrega incertidumbre sobre dónde y cuándo el control está en manos del módulo frente al intérprete.

Los hilos QT operan con el GIL liberado. Los subprocesos QT pueden ejecutar el código de la biblioteca QT (y otro código de módulo compilado que no adquiere el GIL) al mismo tiempo. Sin embargo, el código Python ejecutado dentro del contexto de un hilo QT aún adquiere el GIL, y ahora debe administrar dos conjuntos de lógica para bloquear su código.

Al final, tanto los subprocesos de QT como los de Python son envoltorios de los subprocesos del sistema. Los subprocesos de Python son marginalmente más seguros de usar, ya que aquellas partes que no están escritas en Python (implícitamente usando el GIL) usan el GIL en cualquier caso (aunque la advertencia anterior todavía se aplica).

E / S sin bloqueo

Los subprocesos añaden una complejidad extraordinaria a su aplicación. Especialmente cuando se trata de la ya compleja interacción entre el intérprete de Python y el código del módulo compilado. Si bien a muchos les resulta difícil seguir la programación basada en eventos, la E / S sin bloqueo basada en eventos suele ser mucho menos difícil de razonar que los hilos.

Con E / S asincrónicas, siempre puede estar seguro de que, para cada descriptor abierto, la ruta de ejecución es coherente y ordenada. Obviamente, hay cuestiones que deben abordarse, como qué hacer cuando el código que depende de un canal abierto depende además de los resultados del código que se llamará cuando otro canal abierto devuelva datos.

Una buena solución para E / S sin bloqueo basadas en eventos es la nueva biblioteca Diesel . Está restringido a Linux en este momento, pero es extraordinariamente rápido y bastante elegante.

También vale la pena dedicar tiempo a aprender pyevent , un envoltorio de la maravillosa biblioteca libevent, que proporciona un marco básico para la programación basada en eventos utilizando el método más rápido disponible para su sistema (determinado en el momento de la compilación).

Jeff Ober
fuente
Re Twisted, etc .: Utilizo una biblioteca de terceros que hace las cosas de red; Me gustaría evitar parchearlo. Pero seguiré investigando eso, gracias.
balpha
2
En realidad, nada pasa por alto el GIL. Pero Python libera el GIL durante las operaciones de E / S. Python también lanza el GIL cuando se 'transfiere' a los módulos compilados, que son responsables de adquirir / liberar el GIL ellos mismos.
Jeff Ober
2
La actualización es simplemente incorrecta. El código Python se ejecuta exactamente de la misma manera en un hilo de Python que en un QThread. Usted adquiere el GIL cuando ejecuta código Python (y luego Python administra la ejecución entre subprocesos), lo libera cuando ejecuta código C ++. No hay ninguna diferencia.
Lukáš Lalinský
1
No, el punto es que no importa cómo cree el hilo, al intérprete de Python no le importa. Lo único que le importa es que puede adquirir el GIL y después de X instrucciones puede liberarlo / readquirirlo. Por ejemplo, puede usar ctypes para crear una devolución de llamada desde una biblioteca C, que se llamará en un hilo separado, y el código funcionará bien sin siquiera saber que es un hilo diferente. Realmente no hay nada especial en el módulo de hilo.
Lukáš Lalinský
1
Estabas diciendo en qué se diferencia QThread con respecto al bloqueo y cómo "tienes que administrar dos conjuntos de lógica para bloquear tu código". Lo que estoy diciendo es que no es diferente en absoluto. Puedo usar ctypes y pthread_create para iniciar el hilo, y funcionará exactamente de la misma manera. El código Python simplemente no tiene que preocuparse por el GIL.
Lukáš Lalinský
21

La ventaja QThreades que está integrado con el resto de la biblioteca Qt. Es decir, los métodos con reconocimiento de subprocesos en Qt necesitarán saber en qué subproceso se ejecutan y, para mover objetos entre subprocesos, deberá usarlos QThread. Otra característica útil es ejecutar su propio bucle de eventos en un hilo.

Si está accediendo a un servidor HTTP, debería considerarlo QNetworkAccessManager.

Lukáš Lalinský
fuente
1
Aparte de lo que comenté sobre la respuesta de Jeff Ober, QNetworkAccessManagerparece prometedora. Gracias.
Balpha
13

Me hice la misma pregunta cuando trabajaba en PyTalk .

Si está usando Qt, necesita usar QThreadpara poder usar el marco Qt y especialmente el sistema de señal / ranura.

Con el motor de señal / ranura, podrás hablar de un hilo a otro y con cada parte de tu proyecto.

Además, no hay muchas dudas sobre el rendimiento sobre esta elección, ya que ambos son enlaces de C ++.

Aquí está mi experiencia con PyQt y thread.

Te animo a usar QThread.

Natim
fuente
9

Jeff tiene algunos buenos puntos. Solo un hilo principal puede realizar actualizaciones de GUI. Si necesita actualizar la GUI desde dentro del hilo, las señales de conexión en cola de Qt-4 facilitan el envío de datos a través de hilos y se invocarán automáticamente si está utilizando QThread; No estoy seguro de si lo serán si usa subprocesos de Python, aunque es fácil agregar un parámetro a connect().

Kaleb Pederson
fuente
5

Realmente no puedo recomendar tampoco, pero puedo intentar describir las diferencias entre los hilos CPython y Qt.

En primer lugar, los subprocesos de CPython no se ejecutan al mismo tiempo, al menos no el código Python. Sí, crean subprocesos del sistema para cada subproceso de Python, sin embargo, solo se permite que se ejecute el subproceso que actualmente tiene el bloqueo de intérprete global (las extensiones C y el código FFI pueden omitirlo, pero el código de bytes de Python no se ejecuta mientras el subproceso no contiene GIL).

Por otro lado, tenemos subprocesos Qt, que son básicamente capas comunes sobre subprocesos del sistema, no tienen bloqueo de intérprete global y, por lo tanto, son capaces de ejecutarse simultáneamente. No estoy seguro de cómo PyQt lo maneja, sin embargo, a menos que sus subprocesos Qt llamen al código Python, deberían poder ejecutarse simultáneamente (excluya varios bloqueos adicionales que podrían implementarse en varias estructuras).

Para un ajuste más fino, puede modificar la cantidad de instrucciones de código de bytes que se interpretan antes de cambiar la propiedad de GIL; los valores más bajos significan más cambio de contexto (y posiblemente una mayor capacidad de respuesta) pero un menor rendimiento por hilo individual (los cambios de contexto tienen su costo, si intente cambiar cada pocas instrucciones que no ayuden a acelerar).

Espero que te ayude con tus problemas :)

p_l
fuente
7
Es importante tener en cuenta aquí: PyQt QThreads acepta el bloqueo de intérprete global . Todo el código Python bloquea el GIL y cualquier QThreads que ejecute en PyQt ejecutará código Python. (Si no es así, en realidad no está utilizando la parte "Py" de PyQt :). Si elige diferir de ese código Python a una biblioteca C externa, se lanzará GIL, pero eso es cierto independientemente de si usa un hilo de Python o un hilo de Qt.
quark
Eso fue realmente lo que traté de transmitir, que todo el código Python toma el bloqueo, pero no importa para el código C / C ++ que se ejecuta en un hilo separado
p_l
0

No puedo comentar sobre las diferencias exactas entre los hilos de Python y PyQt, pero yo he estado haciendo lo que estás tratando de hacer uso de QThread, QNetworkAcessManagery asegurándose de llamada QApplication.processEvents(), mientras que el hilo está vivo. Si la capacidad de respuesta de la GUI es realmente el problema que está tratando de resolver, lo último lo ayudará.

brianz
fuente
1
QNetworkAcessManagerno requiere un hilo o processEvents. Utiliza operaciones de E / S asincrónicas.
Lukáš Lalinský
Vaya ... sí, estoy usando una combinación de QNetworkAcessManagery httplib2. Mi código asincrónico usa httplib2.
brianz