Multithreading em Python com exemplo: Aprenda GIL em Python
O que é um fio?
Um thread é uma unidade de execução em programação simultânea. Multithreading é uma técnica que permite que uma CPU execute muitas tarefas de um processo ao mesmo tempo. Esses threads podem ser executados individualmente enquanto compartilham seus recursos de processo.
O que é um Processo?
Um processo é basicamente o programa em execução. Quando você inicia um aplicativo no seu computador (como um navegador ou editor de texto), o sistema operacional cria um processo.
O que é multithreading Python?
Multithreading em Python a programação é uma técnica bem conhecida na qual vários threads em um processo compartilham seu espaço de dados com o thread principal, o que torna o compartilhamento de informações e a comunicação dentro dos threads fácil e eficiente. Threads são mais leves que processos. Multi threads podem ser executados individualmente enquanto compartilham seus recursos de processo. O objetivo do multithreading é executar várias tarefas e células funcionais ao mesmo tempo.
O que é multiprocessamento?
Multiprocessamento permite que você execute vários processos não relacionados simultaneamente. Esses processos não compartilham seus recursos e se comunicam através do IPC.
Python Multithreading x Multiprocessamento
Para entender processos e threads, considere este cenário: Um arquivo .exe no seu computador é um programa. Ao abri-lo, o sistema operacional carrega-o na memória e a CPU o executa. A instância do programa que está sendo executada agora é chamada de processo.
Cada processo terá 2 componentes fundamentais:
- O código
- Os Dados
Agora, um processo pode conter uma ou mais subpartes chamadas tópicos. Isso depende da arquitetura do sistema operacional. Você pode pensar em um thread como uma seção do processo que pode ser executada separadamente pelo sistema operacional.
Em outras palavras, é um fluxo de instruções que pode ser executado de forma independente pelo sistema operacional. Threads dentro de um único processo compartilham os dados desse processo e são projetados para trabalhar juntos para facilitar o paralelismo.
Por que usar multithreading?
Multithreading permite dividir um aplicativo em várias subtarefas e executar essas tarefas simultaneamente. Se você usar o multithreading corretamente, a velocidade, o desempenho e a renderização do seu aplicativo poderão ser melhorados.
Python Multithreading
Python suporta construções tanto para multiprocessamento quanto para multithreading. Neste tutorial, você se concentrará principalmente na implementação multithread aplicações com python. Existem dois módulos principais que podem ser usados para manipular threads em Python:
- A fio módulo, e
- A segmentação módulo
No entanto, em python, também existe algo chamado bloqueio de interpretador global (GIL). Não permite muito ganho de desempenho e pode até reduzir o desempenho de alguns aplicativos multithread. Você aprenderá tudo sobre isso nas próximas seções deste tutorial.
Os módulos Thread e Threading
Os dois módulos que você aprenderá neste tutorial são o módulo de thread e os votos de módulo de threading.
No entanto, o módulo thread está obsoleto há muito tempo. Começando com Python 3, foi designado como obsoleto e só é acessível como __fio para compatibilidade com versões anteriores.
Você deve usar o nível superior segmentação módulo para aplicativos que você pretende implantar. O módulo thread foi abordado aqui apenas para fins educacionais.
O Módulo de Thread
A sintaxe para criar um novo thread usando este módulo é a seguinte:
thread.start_new_thread(function_name, arguments)
Tudo bem, agora você cobriu a teoria básica para começar a codificar. Então, abra seu IDLE ou um bloco de notas e digite o seguinte:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Salve o arquivo e pressione F5 para executar o programa. Se tudo foi feito corretamente, esta é a saída que você deverá ver:
Você aprenderá mais sobre as condições de corrida e como lidar com elas nas próximas seções.
EXPLICAÇÃO DO CÓDIGO
- Essas instruções importam o módulo de tempo e thread que são usados para lidar com a execução e o atraso do Python tópicos.
- Aqui, você definiu uma função chamada thread_teste, que será chamado pelo start_new_thread método. A função executa um loop while por quatro iterações e imprime o nome do thread que a chamou. Assim que a iteração for concluída, ele imprime uma mensagem informando que o thread terminou a execução.
- Esta é a seção principal do seu programa. Aqui, basta ligar para o start_new_thread método com o thread_test funcionar como um argumento. Isso criará um novo thread para a função que você passa como argumento e começará a executá-la. Observe que você pode substituir isso (thread_test) com qualquer outra função que você deseja executar como um thread.
O Módulo de Threading
Este módulo é a implementação de alto nível de threading em python e o padrão de fato para gerenciamento de aplicativos multithread. Ele fornece uma ampla gama de recursos quando comparado ao módulo de thread.

Aqui está uma lista de algumas funções úteis definidas neste módulo:
Nome da Função | Descrição |
---|---|
contagem ativa() | Retorna a contagem de Fio objetos que ainda estão vivos |
currentThread () | Retorna o objeto atual da classe Thread. |
enumerar() | Lista todos os objetos Thread ativos. |
isDaemon() | Retorna verdadeiro se o thread for um daemon. |
Está vivo() | Retorna verdadeiro se o thread ainda estiver ativo. |
Métodos de classe de thread | |
começar() | Inicia a atividade de um thread. Ele deve ser chamado apenas uma vez para cada thread porque gerará um erro de tempo de execução se for chamado várias vezes. |
corre() | Este método denota a atividade de um thread e pode ser substituído por uma classe que estende a classe Thread. |
Junte-se() | Ele bloqueia a execução de outro código até que o thread no qual o método join() foi chamado seja encerrado. |
História de fundo: a classe Thread
Antes de começar a codificar programas multithread usando o módulo threading, é crucial entender sobre a classe Thread. A classe thread é a classe primária que define o modelo e as operações de um thread em python.
A maneira mais comum de criar um aplicativo python multithread é declarar uma classe que estende a classe Thread e substitui seu método run().
A classe Thread, em resumo, significa uma sequência de código que é executada em um fio de controle.
Portanto, ao escrever um aplicativo multithread, você fará o seguinte:
- defina uma classe que estenda a classe Thread
- Substituir o __init__ construtor
- Substituir o corre() método
Depois que um objeto thread for criado, o começar() método pode ser usado para iniciar a execução desta atividade e o Junte-se() O método pode ser usado para bloquear todos os outros códigos até que a atividade atual termine.
Agora, vamos tentar usar o módulo threading para implementar seu exemplo anterior. Mais uma vez, acenda seu IDLE e digite o seguinte:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
Esta será a saída quando você executar o código acima:
EXPLICAÇÃO DO CÓDIGO
- Esta parte é a mesma do nosso exemplo anterior. Aqui, você importa o módulo time e thread que são usados para manipular a execução e os atrasos do Python tópicos.
- Nesta parte, você está criando uma classe chamada threadtester, que herda ou estende o Fio classe do módulo de threading. Esta é uma das formas mais comuns de criar threads em python. No entanto, você só deve substituir o construtor e o corre() método em seu aplicativo. Como você pode ver no exemplo de código acima, o __init__ o método (construtor) foi substituído. Da mesma forma, você também substituiu o corre() método. Ele contém o código que você deseja executar dentro de um thread. Neste exemplo, você chamou a função thread_test().
- Este é o método thread_test() que assume o valor de i como argumento, diminui em 1 a cada iteração e percorre o restante do código até que i se torne 0. Em cada iteração, ele imprime o nome do thread em execução no momento e dorme por alguns segundos (o que também é considerado um argumento ).
- thread1 = threadtester(1, “First Thread”, 1) Aqui estamos criando uma thread e passando os três parâmetros que declaramos em __init__. O primeiro parâmetro é o id do thread, o segundo parâmetro é o nome do thread e o terceiro parâmetro é o contador, que determina quantas vezes o loop while deve ser executado.
- thread2.start()O método start é usado para iniciar a execução de um thread. Internamente, a função start() chama o método run() da sua classe.
- thread3.join() O método join() bloqueia a execução de outro código e espera até que o thread em que foi chamado termine.
Como você já sabe, as threads que estão no mesmo processo têm acesso à memória e aos dados desse processo. Como resultado, se mais de um thread tentar alterar ou acessar os dados simultaneamente, poderão ocorrer erros.
Na próxima seção, você verá os diferentes tipos de complicações que podem surgir quando threads acessam dados e seções críticas sem verificar as transações de acesso existentes.
Impasses e condições de corrida
Antes de aprender sobre impasses e condições de corrida, será útil entender algumas definições básicas relacionadas à programação simultânea:
- Seção CríticaÉ um fragmento de código que acessa ou modifica variáveis compartilhadas e deve ser executado como uma transação atômica.
- Context SwitchIt é o processo que uma CPU segue para armazenar o estado de um thread antes de mudar de uma tarefa para outra, para que possa ser retomada do mesmo ponto posteriormente.
Impasses
Impasses são o problema mais temido que os desenvolvedores enfrentam ao escrever aplicativos simultâneos/multithread em python. A melhor maneira de entender os impasses é usar o problema clássico de exemplo da ciência da computação conhecido como Para Refeições PhiloProblema de Sopher.
A definição do problema para os filósofos do jantar é a seguinte:
Cinco filósofos estão sentados numa mesa redonda com cinco pratos de espaguete (uma espécie de macarrão) e cinco garfos, conforme mostra o diagrama.

A qualquer momento, um filósofo deve estar comendo ou pensando.
Além disso, um filósofo deve pegar os dois garfos adjacentes a ele (isto é, os garfos esquerdo e direito) antes de poder comer o espaguete. O problema do impasse ocorre quando todos os cinco filósofos escolhem os garfos certos simultaneamente.
Como cada um dos filósofos tem um garfo, todos esperarão que os outros pousem o garfo. Como resultado, nenhum deles poderá comer espaguete.
Da mesma forma, em um sistema simultâneo, ocorre um impasse quando diferentes threads ou processos (filósofos) tentam adquirir os recursos compartilhados do sistema (forks) ao mesmo tempo. Como resultado, nenhum dos processos tem chance de ser executado, pois estão aguardando outro recurso mantido por algum outro processo.
Condições da corrida
Uma condição de corrida é um estado indesejado de um programa que ocorre quando um sistema executa duas ou mais operações simultaneamente. Por exemplo, considere este loop for simples:
i=0; # a global variable for x in range(100): print(i) i+=1;
Se você criar n número de threads que executam este código de uma vez, você não pode determinar o valor de i (que é compartilhado pelos threads) quando o programa termina a execução. Isso ocorre porque em um ambiente multithreading real, os threads podem se sobrepor, e o valor de i que foi recuperado e modificado por um thread pode mudar quando algum outro thread o acessa.
Estas são as duas principais classes de problemas que podem ocorrer em um aplicativo Python multithread ou distribuído. Na próxima seção, você aprenderá como superar esse problema sincronizando threads.
Synccronizando tópicos
Para lidar com condições de corrida, impasses e outros problemas baseados em thread, o módulo threading fornece o Travar objeto. A ideia é que quando uma thread deseja acessar um recurso específico, ela adquira um bloqueio para esse recurso. Depois que um thread bloqueia um recurso específico, nenhum outro thread pode acessá-lo até que o bloqueio seja liberado. Como resultado, as alterações no recurso serão atômicas e as condições de corrida serão evitadas.
Um bloqueio é uma primitiva de sincronização de baixo nível implementada pelo __fio módulo. A qualquer momento, um bloqueio pode estar em um dos 2 estados: trancado or desbloqueado. Ele suporta dois métodos:
- adquirir()Quando o estado de bloqueio é desbloqueado, chamar o método adquirir() mudará o estado para bloqueado e retornará. No entanto, se o estado estiver bloqueado, a chamada para adquirir() será bloqueada até que o método release() seja chamado por algum outro thread.
- liberar()O método release() é usado para definir o estado como desbloqueado, ou seja, para liberar um bloqueio. Pode ser chamado por qualquer thread, não necessariamente aquela que adquiriu o bloqueio.
Aqui está um exemplo de uso de bloqueios em seus aplicativos. Acenda seu IDLE e digite o seguinte:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Agora, aperte F5. Você deverá ver uma saída como esta:
EXPLICAÇÃO DO CÓDIGO
- Aqui, você está simplesmente criando um novo bloqueio chamando o threading.Lock () função de fábrica. Internamente, Lock() retorna uma instância da classe Lock concreta mais eficaz que é mantida pela plataforma.
- Na primeira instrução, você adquire o bloqueio chamando o método adquirir(). Quando o bloqueio for concedido, você imprime “bloqueio adquirido” para o console. Depois que todo o código que você deseja que o thread execute tenha concluído a execução, você libera o bloqueio chamando o método release().
A teoria é boa, mas como saber se a fechadura realmente funcionou? Se você observar a saída, verá que cada uma das instruções print está imprimindo exatamente uma linha por vez. Lembre-se de que, em um exemplo anterior, as saídas de print eram aleatórias porque vários threads estavam acessando o método print() ao mesmo tempo. Aqui, a função print é chamada somente após o bloqueio ser adquirido. Assim, as saídas são exibidas uma de cada vez e linha por linha.
Além dos bloqueios, o python também oferece suporte a alguns outros mecanismos para lidar com a sincronização de threads, conforme listado abaixo:
- RLocks
- Semaphores
- Condições
- Eventos, e
- Barreiras
Bloqueio global de intérprete (e como lidar com isso)
Antes de entrar nos detalhes do GIL do python, vamos definir alguns termos que serão úteis para a compreensão da próxima seção:
- Código vinculado à CPU: refere-se a qualquer trecho de código que será executado diretamente pela CPU.
- Código vinculado a E/S: pode ser qualquer código que acesse o sistema de arquivos através do sistema operacional
- CPython: é a referência implementação of Python e pode ser descrito como o intérprete escrito em C e Python (linguagem de programação).
Em que está o GIL Python?
Bloqueio global de intérprete (GIL) em python é um bloqueio de processo ou mutex usado ao lidar com os processos. Garante que um thread possa acessar um recurso específico por vez e também evita o uso de objetos e bytecodes de uma só vez. Isso beneficia os programas de thread único em um aumento de desempenho. GIL em python é muito simples e fácil de implementar.
Um bloqueio pode ser usado para garantir que apenas um thread tenha acesso a um recurso específico em um determinado momento.
Uma das características do Python é que ele usa um bloqueio global em cada processo do interpretador, o que significa que cada processo trata o próprio interpretador Python como um recurso.
Por exemplo, suponha que você tenha escrito um programa python que usa dois threads para executar operações de CPU e de 'E/S'. Quando você executa este programa, acontece o seguinte:
- O interpretador python cria um novo processo e gera os threads
- Quando o thread-1 começar a ser executado, ele primeiro adquirirá o GIL e o bloqueará.
- Se o thread-2 quiser executar agora, ele terá que esperar a liberação do GIL, mesmo que outro processador esteja livre.
- Agora, suponha que o thread-1 esteja aguardando uma operação de E/S. Neste momento, ele irá liberar o GIL e o thread-2 irá adquiri-lo.
- Depois de concluir as operações de E/S, se o thread-1 quiser executar agora, ele terá que esperar novamente que o GIL seja liberado pelo thread-2.
Devido a isso, apenas um thread pode acessar o interpretador a qualquer momento, o que significa que haverá apenas um thread executando código python em um determinado momento.
Isso é bom em um processador de núcleo único porque ele usaria divisão de tempo (veja a primeira seção deste tutorial) para lidar com os threads. No entanto, no caso de processadores multi-core, uma função vinculada à CPU executada em vários threads terá um impacto considerável na eficiência do programa, uma vez que na verdade não usará todos os núcleos disponíveis ao mesmo tempo.
Por que o GIL foi necessário?
O CPython garbage collector usa uma técnica eficiente de gerenciamento de memória conhecida como contagem de referência. Veja como funciona: Cada objeto em python tem uma contagem de referência, que é aumentada quando é atribuída a um novo nome de variável ou adicionada a um contêiner (como tuplas, listas, etc.). Da mesma forma, a contagem de referência é diminuída quando a referência sai do escopo ou quando a instrução del é chamada. Quando a contagem de referência de um objeto atinge 0, ele é coletado como lixo e a memória alocada é liberada.
Mas o problema é que a variável de contagem de referência está sujeita a condições de corrida como qualquer outra variável global. Para resolver este problema, os desenvolvedores do python decidiram usar o bloqueio global do interpretador. A outra opção era adicionar um bloqueio a cada objeto, o que resultaria em conflitos e aumentaria a sobrecarga das chamadas adquirir() e liberar().
Portanto, GIL é uma restrição significativa para programas python multithread que executam operações pesadas vinculadas à CPU (tornando-os efetivamente de thread único). Se você quiser usar vários núcleos de CPU em seu aplicativo, use o multiprocessamento módulo em vez disso.
Resumo
- Python suporta 2 módulos para multithreading:
- __fio módulo: fornece uma implementação de baixo nível para threading e é obsoleto.
- módulo de threading: fornece uma implementação de alto nível para multithreading e é o padrão atual.
- Para criar um thread usando o módulo threading, você deve fazer o seguinte:
- Crie uma classe que estenda o Fio classe.
- Substitua seu construtor (__init__).
- Substituir seu corre() método.
- Crie um objeto desta classe.
- Um thread pode ser executado chamando o método começar() método.
- A Junte-se() O método pode ser usado para bloquear outros threads até que este thread (aquele no qual o join foi chamado) termine a execução.
- Uma condição de corrida ocorre quando vários threads acessam ou modificam um recurso compartilhado ao mesmo tempo.
- Pode ser evitado por Synccronizando os fios.
- Python suporta 6 maneiras de sincronizar threads:
- Locks
- RLocks
- Semaphores
- Condições
- Eventos, e
- Barreiras
- Os bloqueios permitem que apenas um thread específico que adquiriu o bloqueio entre na seção crítica.
- Um bloqueio possui 2 métodos principais:
- adquirir(): Define o estado de bloqueio para trancado. Se chamado em um objeto bloqueado, ele será bloqueado até que o recurso seja liberado.
- liberar(): Define o estado de bloqueio para desbloqueado e retorna. Se chamado em um objeto desbloqueado, ele retorna falso.
- O bloqueio global do interpretador é um mecanismo através do qual apenas 1 CPython o processo do interpretador pode ser executado por vez.
- Foi usado para facilitar a funcionalidade de contagem de referência do CPythoncoletor de lixo de s.
- Para fazer Python Para aplicativos com operações pesadas que exigem muita CPU, você deve usar o módulo de multiprocessamento.