Como os threads funcionam no Python e quais são as armadilhas específicas comuns do threading do Python?

85

Tenho tentado entender como os threads funcionam no Python e é difícil encontrar boas informações sobre como eles funcionam. Posso estar perdendo um link ou algo assim, mas parece que a documentação oficial não é muito completa sobre o assunto e não consegui encontrar um bom artigo.

Pelo que eu posso dizer, apenas um thread pode estar em execução por vez, e o thread ativo muda a cada 10 instruções ou mais?

Onde há uma boa explicação ou você pode fornecer uma? Também seria muito bom estar ciente dos problemas comuns que você encontra ao usar threads com Python.

jdd
fonte

Respostas:

51

Sim, por causa do Global Interpreter Lock (GIL), só pode executar um thread de cada vez. Aqui estão alguns links com alguns insights sobre isso:

Do último link, uma citação interessante:

Deixe-me explicar o que tudo isso significa. Threads são executados dentro da mesma máquina virtual e, portanto, executados na mesma máquina física. Os processos podem ser executados na mesma máquina física ou em outra máquina física. Se você arquitetar seu aplicativo em torno de threads, não terá feito nada para acessar várias máquinas. Portanto, você pode escalar para quantos núcleos estiverem em uma única máquina (que serão alguns com o tempo), mas para realmente alcançar escalas web, você precisará resolver o problema de várias máquinas de qualquer maneira.

Se você deseja usar vários núcleos, pyprocessing define uma API baseada em processo para fazer a paralelização real. O PEP também inclui alguns benchmarks interessantes.

Peter Hoffmann
fonte
1
Realmente, um comentário sobre a citação smoothspan: certamente o threading Python efetivamente limita você a um núcleo, mesmo se a máquina tiver vários? Pode haver benefícios de multicore, pois o próximo thread pode estar pronto para funcionar sem uma troca de contexto, mas seus threads Python nunca podem fazer uso de> 1 núcleo por vez.
James Brady
2
Correto, os threads do python são praticamente limitados a um único núcleo, A MENOS que um módulo C interaja bem com o GIL e execute seu próprio thread nativo.
Arafangion
Na verdade, vários núcleos tornam os threads menos eficientes, pois há muita rotatividade ao verificar se cada thread pode acessar o GIL. Mesmo com o novo GIL, o desempenho é ainda pior ... dabeaz.com/python/NewGIL.pdf
Básico
2
Observe que as considerações do GIL não se aplicam a todos os intérpretes. Tanto quanto eu sei, o IronPython e o Jython funcionam sem um GIL, permitindo que seu código faça um uso mais eficaz de hardware com vários processadores. Como Arafangion mencionou, o interpretador CPython também pode rodar apropriadamente multi-threaded se o código que não precisa de acesso aos itens de dados Python libera o bloqueio, então o adquire novamente antes de retornar.
holdenweb
O que causa uma mudança de contexto entre os threads em Python? É baseado em interrupções do cronômetro? Bloqueio ou uma chamada de rendimento específica?
CMCDragonkai
36

Python é uma linguagem bastante fácil de usar, mas existem ressalvas. A maior coisa que você precisa saber é o Global Interpreter Lock. Isso permite que apenas um thread acesse o intérprete. Isso significa duas coisas: 1) você raramente usa uma instrução de bloqueio em python e 2) se deseja tirar proveito de sistemas com vários processadores, é necessário usar processos separados. EDIT: Também devo salientar que você pode colocar parte do código em C / C ++ se quiser contornar o GIL também.

Portanto, você precisa reconsiderar por que deseja usar threads. Se você deseja paralelizar seu aplicativo para aproveitar as vantagens da arquitetura dual-core, deve considerar dividir seu aplicativo em vários processos.

Se você deseja melhorar a capacidade de resposta, você deve CONSIDERAR o uso de threads. No entanto, existem outras alternativas, nomeadamente microthreading . Existem também algumas estruturas que você deve examinar:

Jason Baker
fonte
@JS - Fixo. Essa lista estava desatualizada de qualquer maneira.
Jason Baker
Parece errado para mim que você precise de vários processos - com toda a sobrecarga que isso acarreta - para aproveitar as vantagens de um sistema multi-core. Temos alguns servidores com 32 núcleos lógicos - preciso de 32 processos para usá-los com eficiência? Loucura
Básico de
@Basic - A sobrecarga em iniciar um processo versus iniciar um thread atualmente é mínima. Suponho que você possa começar a ver problemas se estivermos falando de milhares de consultas por segundo, mas então eu questionaria a escolha do Python para um serviço tão ocupado em primeiro lugar.
Jason Baker
20

Abaixo está um exemplo básico de threading. Ele irá gerar 20 threads; cada thread produzirá seu número de thread. Execute-o e observe a ordem de impressão.

import threading
class Foo (threading.Thread):
    def __init__(self,x):
        self.__x = x
        threading.Thread.__init__(self)
    def run (self):
          print str(self.__x)

for x in xrange(20):
    Foo(x).start()

Como você indicou, os threads do Python são implementados por meio de divisão de tempo. É assim que eles obtêm o efeito "paralelo".

Em meu exemplo, minha classe Foo estende thread, então implemento o runmétodo, que é para onde vai o código que você gostaria de executar em um thread. Para iniciar o thread, você chama start()no objeto thread, que invocará automaticamente o runmétodo ...

Claro, isso é apenas o básico. Eventualmente, você desejará aprender sobre semáforos, mutexes e bloqueios para sincronização de threads e passagem de mensagens.

mmattax
fonte
10

Use threads em python se os trabalhadores individuais estiverem fazendo operações vinculadas a E / S. Se você está tentando escalar em vários núcleos em uma máquina, encontre uma boa estrutura IPC para python ou escolha uma linguagem diferente.

Ben McNiel
fonte
5

Nota: sempre que menciono, threadquero dizer especificamente threads em python até que seja explicitamente declarado.

Threads funcionam de maneira um pouco diferente em python se você estiver vindo de C/C++segundo plano. No python, apenas um thread pode estar em execução em um determinado momento. Isso significa que os threads no python não podem realmente aproveitar o poder de vários núcleos de processamento, pois, por design, não é possível que os threads sejam executados paralelamente em vários núcleos.

Como o gerenciamento de memória em python não é seguro para threads, cada thread requer um acesso exclusivo às estruturas de dados no interpretador python. Esse acesso exclusivo é adquirido por um mecanismo chamado (bloqueio global do interpretador) .GIL

Why does python use GIL?

A fim de evitar que vários threads acessem o estado do interpretador simultaneamente e corrompam o estado do interpretador.

A ideia é sempre que um thread está sendo executado (mesmo que seja o thread principal) , um GIL é adquirido e após algum intervalo de tempo predefinido o GIL é liberado pelo thread atual e readquirido por algum outro thread (se houver).

Why not simply remove GIL?

Não que seja impossível remover o GIL, é apenas que, no processo de fazer isso, acabamos colocando vários bloqueios dentro do interpretador para serializar o acesso, o que torna até mesmo um único aplicativo encadeado menos performante.

portanto, o custo de remoção do GIL é compensado pela redução do desempenho de um aplicativo thread único, o que nunca é desejado.

So when does thread switching occurs in python?

A troca de thread ocorre quando GIL é lançado. Então, quando GIL é lançado? Existem dois cenários a serem considerados.

Se um Thread estiver fazendo operações vinculadas à CPU (processamento de imagem Ex).

Em versões mais antigas do python, a troca de thread costumava ocorrer após um não fixo de instruções do python. Por padrão, era definido como 100. Descobriu-se que não é uma política muito boa decidir quando a troca deve ocorrer, pois o tempo gasto na execução de uma única instrução pode ir de milissegundo até mesmo um segundo. Portanto, liberar GIL após todas as 100instruções, independentemente do tempo que levem para executar, é uma política ruim.

Em novas versões, em vez de usar a contagem de instruções como uma métrica para alternar o thread, um intervalo de tempo configurável é usado. O intervalo de troca padrão é de 5 milissegundos. Você pode obter o intervalo de troca atual usando sys.getswitchinterval(). Isso pode ser alterado usandosys.setswitchinterval()

Se um thread está fazendo algumas operações vinculadas de IO (acesso ao sistema de arquivos Ex ou
IO de rede)

GIL é liberado sempre que o thread está aguardando a conclusão de alguma operação de E / S.

Which thread to switch to next?

O intérprete não tem seu próprio agendador. Qual thread é agendado no final do intervalo é uma decisão do sistema operacional. .

Anekix
fonte
3

Uma solução fácil para o GIL é o módulo de multiprocessamento . Ele pode ser usado como um substituto para o módulo de threading, mas usa vários processos de intérprete em vez de threads. Por causa disso, há um pouco mais de sobrecarga do que simples threading para coisas simples, mas oferece a vantagem de paralelização real, se necessário. Ele também pode ser facilmente dimensionado para várias máquinas físicas.

Se você precisa de paralelização em grande escala, então eu procuraria mais, mas se você quiser apenas dimensionar para todos os núcleos de um computador ou alguns diferentes sem todo o trabalho que seria necessário para implementar uma estrutura mais abrangente, então isso é para você .

vou t
fonte
2

Tente lembrar que o GIL está configurado para pesquisar de vez em quando para mostrar a aparência de várias tarefas. Essa configuração pode ser ajustada, mas ofereço a sugestão de que deve haver algum trabalho que os encadeamentos estejam realizando ou muitas mudanças de contexto causarão problemas.

Eu iria mais longe a ponto de sugerir vários pais em processadores e tentar manter trabalhos semelhantes no (s) mesmo (s) núcleo (s).

phreaki
fonte