Threading em um aplicativo PyQt: Use threads Qt ou threads Python?

116

Estou escrevendo um aplicativo GUI que regularmente recupera dados por meio de uma conexão da web. Como essa recuperação demora um pouco, isso faz com que a IU pare de responder durante o processo de recuperação (não pode ser dividida em partes menores). É por isso que eu gostaria de terceirizar a conexão da web para um thread de trabalho separado.

[Sim, eu sei, agora tenho dois problemas .]

Enfim, o aplicativo usa PyQt4, então gostaria de saber qual é a melhor escolha: Usar threads do Qt ou usar o threadingmódulo Python ? Quais são as vantagens / desvantagens de cada um? Ou você tem uma sugestão totalmente diferente?

Edit (re bounty): Embora a solução em meu caso particular provavelmente seja usar uma solicitação de rede sem bloqueio, como Jeff Ober e Lukáš Lalinský sugeriram (então, basicamente, deixando os problemas de simultaneidade para a implementação de rede), ainda gostaria de mais resposta aprofundada à questão geral:

Quais são as vantagens e desvantagens de usar threads de PyQt4 (ou seja, Qt) em vez de threads de Python nativos (do threadingmódulo)?


Editar 2: Obrigado a todos por suas respostas. Embora não haja 100% de concordância, parece haver um consenso geral de que a resposta é "use Qt", já que a vantagem disso é a integração com o resto da biblioteca, embora não cause desvantagens reais.

Para qualquer um que queira escolher entre as duas implementações de threading, recomendo fortemente que leiam todas as respostas fornecidas aqui, incluindo o thread da lista de discussão PyQt para o qual o abade se vincula.

Houve várias respostas que considerei para a recompensa; no final, escolhi o abade para a referência externa muito relevante; foi, no entanto, uma decisão difícil.

Obrigado novamente.

balpha
fonte

Respostas:

106

Isso foi discutido não muito tempo atrás na lista de discussão do PyQt. Citando comentários de Giovanni Bajo sobre o assunto:

É basicamente o mesmo. A principal diferença é que QThreads são melhor integrados com Qt (sinais / slots assíncronos, loop de evento, etc.). Além disso, você não pode usar Qt de um thread Python (você não pode, por exemplo, postar um evento no thread principal através de QApplication.postEvent): você precisa de um QThread para que isso funcione.

Uma regra geral pode ser usar QThreads se você for interagir de alguma forma com o Qt e usar threads Python de outra forma.

E alguns comentários anteriores sobre este assunto do autor de PyQt: "ambos são wrappers em torno das mesmas implementações de thread nativas". E ambas as implementações usam GIL da mesma maneira.

abade
fonte
2
Boa resposta, mas acho que você deveria usar o botão de citação para mostrar claramente onde você está de fato, não resumindo, mas citando Giovanni Bajo da lista de discussão :)
c089
2
Eu me pergunto por que você não pode postar eventos no thread principal por meio de QApplication.postEvent () e precisa de um QThread para isso? Acho que vi pessoas fazendo isso e funcionou.
Trilarion
1
Eu chamei QCoreApplication.postEventde um thread Python a uma taxa de 100 vezes por segundo, em um aplicativo que roda em plataforma cruzada e foi testado por 1000 horas. Eu nunca vi nenhum problema com isso. Acho que está tudo bem, desde que o objeto de destino esteja localizado no MainThread ou em um QThread. Eu também embrulhei em uma boa biblioteca, veja qtutils .
três_pineapples
2
Dada a natureza altamente votada desta pergunta e resposta, acho que vale a pena apontar uma resposta recente do SO por ekhumoro que discorre sobre as condições nas quais é seguro usar certos métodos Qt de threads do Python. Isso coincide com o comportamento observado que foi visto por mim e @Trilarion.
three_pineapples de
33

Os threads do Python serão mais simples e seguros e, como se trata de um aplicativo baseado em I / O, eles são capazes de contornar o GIL. Dito isso, você considerou o I / O sem bloqueio usando sockets / select Twisted ou sem bloqueio?

EDIT: mais em tópicos

Tópicos Python

Os threads do Python são threads do sistema. No entanto, o Python usa um bloqueio de intérprete global (GIL) para garantir que o intérprete execute apenas um bloco de determinado tamanho de instruções de código de byte por vez. Felizmente, o Python libera o GIL durante as operações de entrada / saída, tornando os threads úteis para simular I / O sem bloqueio.

Advertência importante: isso pode ser enganoso, pois o número de instruções de código de byte não corresponde ao número de linhas em um programa. Mesmo uma única atribuição pode não ser atômica em Python, portanto, um bloqueio mutex é necessário para qualquer bloco de código que deve ser executado atomicamente, mesmo com o GIL.

Tópicos QT

Quando o Python transfere o controle para um módulo compilado de terceiros, ele libera o GIL. Torna-se responsabilidade do módulo garantir a atomicidade quando necessário. Quando o controle é passado de volta, Python usará o GIL. Isso pode tornar o uso de bibliotecas de terceiros em conjunto com threads confuso. É ainda mais difícil usar uma biblioteca de threading externa porque adiciona incerteza sobre onde e quando o controle está nas mãos do módulo versus o interpretador.

Threads QT operam com o GIL lançado. Threads QT são capazes de executar código de biblioteca QT (e outro código de módulo compilado que não adquire o GIL) simultaneamente. No entanto, o código Python executado dentro do contexto de uma thread QT ainda adquire o GIL, e agora você tem que gerenciar dois conjuntos de lógica para bloquear seu código.

No final, ambos os threads QT e Python são wrappers em torno dos threads do sistema. Threads Python são um pouco mais seguros de usar, já que as partes que não são escritas em Python (usando implicitamente o GIL) usam o GIL em qualquer caso (embora a advertência acima ainda se aplique).

E / S sem bloqueio

Threads adicionam complexidade extraordinária ao seu aplicativo. Especialmente ao lidar com a interação já complexa entre o interpretador Python e o código do módulo compilado. Embora muitos achem a programação baseada em eventos difícil de seguir, a E / S não bloqueadora baseada em eventos costuma ser muito menos difícil de raciocinar do que os threads.

Com a E / S assíncrona, você sempre pode ter certeza de que, para cada descritor aberto, o caminho de execução é consistente e ordenado. Existem, obviamente, questões que devem ser tratadas, como o que fazer quando o código dependendo de um canal aberto depende ainda mais dos resultados do código a ser chamado quando outro canal aberto retorna dados.

Uma boa solução para E / S sem bloqueio baseada em eventos é a nova biblioteca Diesel . Ele está restrito ao Linux no momento, mas é extraordinariamente rápido e bastante elegante.

Também vale a pena aprender pyevent , um invólucro da maravilhosa biblioteca libevent, que fornece uma estrutura básica para programação baseada em eventos usando o método mais rápido disponível para seu sistema (determinado em tempo de compilação).

Jeff Ober
fonte
Re Twisted etc .: Eu uso uma biblioteca de terceiros que faz o trabalho de rede; Eu gostaria de evitar remendos nele. Mas ainda vou analisar isso, obrigado.
balpha
2
Nada realmente ultrapassa o GIL. Mas o Python libera o GIL durante as operações de I / O. O Python também libera o GIL ao 'entregar' aos módulos compilados, que são responsáveis ​​por adquirir / liberar o GIL.
Jeff Ober,
2
A atualização está simplesmente errada. O código Python é executado exatamente da mesma maneira em um thread Python do que em um QThread. Você obtém o GIL quando executa o código Python (e então o Python gerencia a execução entre os threads) e o libera ao executar o código C ++. Não há diferença alguma.
Lukáš Lalinský
1
Não, o ponto é que não importa como você cria o thread, o interpretador Python não se importa. Tudo o que importa é que possa adquirir o GIL e após as instruções do X possa liberá-lo / readquiri-lo. Você pode, por exemplo, usar ctypes para criar um retorno de chamada de uma biblioteca C, que será chamada em uma thread separada, e o código funcionará bem, mesmo sem saber que é uma thread diferente. Não há realmente nada de especial no módulo thread.
Lukáš Lalinský
1
Você estava dizendo como QThread é diferente em relação ao bloqueio e como "você tem que gerenciar dois conjuntos de lógica para bloquear seu código". O que estou dizendo é que não é nada diferente. Posso usar ctypes e pthread_create para iniciar o segmento e funcionará exatamente da mesma maneira. O código Python simplesmente não precisa se preocupar com o GIL.
Lukáš Lalinský
21

A vantagem QThreadé que ele está integrado com o resto da biblioteca Qt. Isto é, os métodos com reconhecimento de thread no Qt precisarão saber em qual thread eles rodam, e para mover objetos entre threads, você precisará usar QThread. Outro recurso útil é executar seu próprio loop de eventos em um thread.

Se você estiver acessando um servidor HTTP, você deve considerar QNetworkAccessManager.

Lukáš Lalinský
fonte
1
Além do que comentei sobre a resposta de Jeff Ober, QNetworkAccessManagerparece promissor. Obrigado.
balpha de
13

Eu me perguntei a mesma coisa quando estava trabalhando no PyTalk .

Se você está usando Qt, você precisa QThreadser capaz de usar o framework Qt e especialmente o sistema de sinal / slot.

Com o motor de sinal / slot, você será capaz de conversar de um tópico para outro e com todas as partes do seu projeto.

Além disso, não há muita questão de desempenho sobre essa escolha, uma vez que ambas são ligações C ++.

Aqui está minha experiência com PyQt e thread.

Eu encorajo você a usar QThread.

Natim
fonte
9

Jeff tem alguns pontos positivos. Apenas um thread principal pode fazer atualizações de GUI. Se você precisar atualizar a GUI de dentro do thread, os sinais de conexão enfileirados do Qt-4 facilitam o envio de dados através dos threads e serão chamados automaticamente se você estiver usando QThread; Não tenho certeza se eles serão se você estiver usando threads Python, embora seja fácil adicionar um parâmetro a connect().

Kaleb Pederson
fonte
5

Também não posso recomendar, mas posso tentar descrever as diferenças entre os threads CPython e Qt.

Em primeiro lugar, os threads de CPython não são executados simultaneamente, pelo menos não o código Python. Sim, eles criam threads de sistema para cada thread do Python, no entanto, apenas a thread que contém o bloqueio global do intérprete pode ser executada (extensões C e código FFI podem contorná-lo, mas o bytecode do Python não é executado enquanto a thread não contém GIL).

Por outro lado, temos threads Qt, que são basicamente camadas comuns sobre threads do sistema, não têm Global Interpreter Lock e, portanto, são capazes de rodar simultaneamente. Não tenho certeza de como o PyQt lida com isso, no entanto, a menos que seus threads Qt chamem o código Python, eles devem ser capazes de executar simultaneamente (exceto vários bloqueios extras que podem ser implementados em várias estruturas).

Para um ajuste fino extra, você pode modificar a quantidade de instruções de bytecode que são interpretadas antes de trocar a propriedade de GIL - valores mais baixos significam mais troca de contexto (e possivelmente maior capacidade de resposta), mas desempenho inferior por thread individual (mudanças de contexto têm seu custo - se você tente mudar a cada poucas instruções, isso não ajuda a acelerar.)

Espero que ajude com seus problemas :)

p_l
fonte
7
É importante observar aqui: os PyQt QThreads usam o Bloqueio global do intérprete . Todo o código Python bloqueia o GIL, e qualquer QThreads que você executar no PyQt estará executando o código Python. (Se não, você não está realmente usando a parte "Py" do PyQt :). Se você optar por adiar esse código Python para uma biblioteca C externa, o GIL será lançado, mas isso é verdade independentemente de você usar um thread Python ou um thread Qt.
quark
Na verdade, foi isso que tentei transmitir, que todo o código Python leva o bloqueio, mas não importa para o código C / C ++ em execução em um thread separado
p_l
0

Não posso comentar sobre as diferenças exatas entre tópicos Python e PyQt, mas eu tenho feito o que você está tentando fazer usando QThread, QNetworkAcessManagere certificando-se de chamada QApplication.processEvents()enquanto o segmento está vivo. Se GUI capacidade de resposta é realmente o problema que está tentando resolver, o mais tarde vai ajudar.

Brianz
fonte
1
QNetworkAcessManagernão requer um tópico ou processEvents. Ele usa operações de E / S assíncronas.
Lukáš Lalinský
Opa ... sim, estou usando uma combinação de QNetworkAcessManagere httplib2. Meu código assíncrono usa httplib2.
brianz