Multithreading em Python com exemplo: Aprenda GIL em Python

⚡ Resumo Inteligente

Multithreading em Python Executa várias threads dentro de um único processo, permitindo que compartilhem memória e trabalhem simultaneamente. O módulo de threading cria e gerencia essas threads, enquanto o Global Interpreter Lock limita o paralelismo verdadeiro, tornando a técnica mais adequada para tarefas com dependência de entrada/saída.

  • 🔘 Thread vs. processo: Uma thread é uma unidade de execução dentro de um processo, e as threads compartilham os dados do processo enquanto são executadas individualmente.
  • ☑️ Dois módulos: O módulo obsoleto _thread oferece chamadas de baixo nível, enquanto o módulo threading de nível superior é o padrão moderno para a construção de threads.
  • Criar tópicos: Estenda a classe Thread, sobrescreva os métodos __init__ e run(), depois chame start() para iniciar e join() para aguardar a conclusão.
  • 🧪 Evite condições de corrida: Um Lock permite que apenas uma thread entre em uma seção crítica por vez, evitando impasses e corrupção de dados compartilhados.
  • 🛠️ Entenda o GIL: O Global Interpreter Lock permite que uma thread seja executada. Python O processamento é feito em lotes de bytecode, portanto, tarefas que exigem muito da CPU necessitam de multiprocessamento.
  • 🤖 Cargas de trabalho de IA: A multithreading acelera etapas de IA com dependência de entrada/saída, como o carregamento de dados, enquanto NumPy e PyTorch libera o GIL para computação paralela.

Multithreading em Python

O Python A linguagem de programação permite o uso de multiprocessamento ou multithreading. Neste tutorial, você aprenderá como escrever aplicações multithread em Python.

O que é um fio?

Uma thread é uma unidade de execução em programação concorrente. Multithreading é uma técnica que permite que uma CPU execute várias tarefas de um mesmo processo simultaneamente. Essas threads podem ser executadas individualmente, compartilhando os recursos do 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 um editor de texto), o sistema operacional cria um processo. processo.

O que é multithreading Python?

Multithreading em Python A programação multithread é uma técnica bem conhecida na qual múltiplas threads em um processo compartilham seu espaço de dados com a thread principal, o que facilita e torna mais eficiente o compartilhamento de informações e a comunicação entre as threads. Threads são mais leves que processos. Múltiplas threads podem executar individualmente enquanto compartilham seus recursos de processo. O objetivo da multithreading é executar múltiplas tarefas e funções simultaneamente.

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. Quando você o abre, o sistema operacional o carrega na memória e a CPU o executa. A instância do programa que está em execução é chamada de processo.

Todo processo possui dois componentes fundamentais:

  • O Code
  • 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 uma thread como uma seção do processo que pode ser executada separadamente pelo sistema operacional.

Em outras palavras, trata-se de um fluxo de instruções que pode ser executado independentemente pelo sistema operacional. Os threads dentro de um mesmo processo compartilham os dados desse processo e são projetados para trabalhar em conjunto, facilitando 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 para multiprocessamento e multithreading. Neste tutorial, você se concentrará principalmente na implementação. multithread aplicações com PythonExistem dois módulos principais que podem ser usados ​​para lidar com threads em Python:

  1. O fio módulo, e
  2. O segmentação módulo

No entanto, em PythonExiste também algo chamado bloqueio global do interpretador (GIL). Ele não proporciona grande ganho de desempenho e pode até mesmo... 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 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 aplicações que você pretende implantar. O módulo de threads foi abordado aqui apenas para fins didáticos.

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:

O Módulo de Thread

Você aprenderá mais sobre as condições da corrida e como lidar com elas nas próximas seções.

O Módulo de Thread

EXPLICAÇÃO DO CÓDIGO

  1. Essas instruções importam os módulos de tempo e thread, que são usados ​​para gerenciar a execução e o atraso do processo. Python tópicos.
  2. Aqui, você definiu uma função chamada thread_teste, que será chamado pelo start_new_thread O método executa um loop while por quatro iterações e imprime o nome da thread que o chamou. Assim que a iteração é concluída, imprime uma mensagem informando que a thread terminou sua execução.
  3. Esta é a seção principal do seu programa. Aqui, basta ligar para o start_new_thread método com o thread_test função como argumento. Isso criará uma nova thread para a função que você passar como argumento e começará a executá-la. Observe que você pode substituir isso (thread_test) por qualquer outra função que você queira executar como uma thread.

O Módulo de Threading

Este módulo é a implementação de alto nível de threads em Python e o padrão de facto para gerenciar aplicações multithread. Ele oferece uma ampla gama de recursos em comparação com o módulo thread.

Estrutura do módulo Threading

Estrutura do módulo Threading

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 programar em ambientes multithread usando o módulo threading, é crucial entender a classe Thread. A classe Thread é a classe principal que define o modelo e as operações de uma thread. Python.

A maneira mais comum de criar um sistema multithread é... Python A aplicação consiste em declarar uma classe que estende a classe Thread e sobrescreve 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:

  1. Defina uma classe que estenda a classe Thread.
  2. Substituir o __init__ construtor
  3. Substituir o corre() método

Depois que um objeto thread for criado, o começar() O 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 o exemplo anterior. Novamente, inicie o 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:

História de fundo: a classe Thread

EXPLICAÇÃO DO CÓDIGO

História de fundo: a classe Thread

  1. Esta parte é igual ao nosso exemplo anterior. Aqui, você importa os módulos `time` e `thread`, que são usados ​​para lidar com a execução e os atrasos do programa. Python tópicos.
  2. Nesta parte, você está criando uma classe chamada threadtester, que herda ou estende o Fio classe do módulo de threading. Esta é uma das maneiras mais comuns de criar threads em PythonNo entanto, você só deve sobrescrever 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().
  3. Este é o método thread_test(), que recebe o valor de i como argumento, decrementa-o em 1 a cada iteração e repete o restante do código até que i se torne 0. Em cada iteração, imprime o nome da thread em execução e aguarda o número de segundos especificado (que também é recebido como argumento).
  4. 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.
  5. thread2.start() O método start é usado para iniciar a execução de uma thread. Internamente, a função start() chama o método run() da sua classe.
  6. 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. Consequentemente, se mais de uma thread tentar alterar ou acessar os dados simultaneamente, podem ocorrer erros.

Na próxima seção, você verá os diferentes tipos de complicações que podem surgir quando threads acessam dados e a seção crítica sem verificar se existem transações de acesso em andamento.

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 concorrente:

  • Seção Crítica: Trata-se de um fragmento de código que acessa ou modifica variáveis ​​compartilhadas e deve ser executado como uma transação atômica.
  • Troca de contexto: É o processo que uma CPU segue para armazenar o estado de uma thread antes de mudar de uma tarefa para outra, de forma que ela possa ser retomada do mesmo ponto posteriormente.

Impasses

Impasses são os problemas mais temidos pelos desenvolvedores ao escrever aplicativos concorrentes/multithread em PythonA melhor maneira de entender impasses é usando o problema clássico da ciência da computação conhecido como impasse. 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 em uma mesa redonda com cinco pratos de espaguete (um tipo de massa) e cinco garfos, conforme mostrado no diagrama.

Para Refeições PhiloProblema de Sophers

Para Refeições PhiloProblema de Sophers

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 simples laço `for`:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Se você criar n Devido ao número de threads que executam este código simultaneamente, não é possível determinar o valor de `i` (que é compartilhado pelas threads) quando o programa termina a execução. Isso ocorre porque, em um ambiente multithread real, as threads podem se sobrepor, e o valor de `i` que foi obtido e modificado por uma thread pode mudar no meio do processo, quando outra thread o acessa.

Essas são as duas principais classes de problemas que podem ocorrer em um ambiente multithread ou distribuído. Python aplicação. 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. Em qualquer momento, uma fechadura pode estar em um dos dois estados: trancado or desbloqueado. Ele suporta dois métodos:

  1. adquirir(): Quando o estado de bloqueio é desbloqueado, chamar o método `acquire()` mudará o estado para bloqueado e retornará. No entanto, se o estado estiver bloqueado, a chamada a `acquire()` será bloqueada até que o método `release()` seja chamado por outra thread.
  2. 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 como usar bloqueios em seus aplicativos. Inicie seu aplicativo. 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:

Synccronizando Threads

EXPLICAÇÃO DO CÓDIGO

Synccronizando Threads

  1. 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.
  2. 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 está correta, mas como saber se o bloqueio realmente funcionou? Se você observar a saída, verá que cada instrução `print` imprime exatamente uma linha por vez. Lembre-se de que, em um exemplo anterior, as saídas do `print` eram aleatórias porque várias threads estavam acessando o método `print()` simultaneamente. Aqui, a função `print` é chamada somente após o bloqueio ser adquirido. Portanto, as saídas são exibidas uma de cada vez e linha por linha.

Além de fechaduras, Python Também oferece suporte a outros mecanismos para lidar com a sincronização de threads, conforme listado abaixo:

  1. RLocks
  2. Semaphores
  3. Condições
  4. Eventos, e
  5. Barreiras

Bloqueio global de intérprete (e como lidar com isso)

Antes de entrar nos detalhes PythonPara entendermos a próxima seção, vamos definir alguns termos que serão úteis:

  1. Código vinculado à CPU: refere-se a qualquer trecho de código que será executado diretamente pela CPU.
  2. Código vinculado a E/S: pode ser qualquer código que acesse o sistema de arquivos por meio do sistema operacional.
  3. 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) in Python GIL é um bloqueio de processo ou mutex usado no gerenciamento de processos. Ele garante que apenas uma thread possa acessar um recurso específico por vez e também impede o uso simultâneo de objetos e bytecodes. Isso beneficia programas de thread única, proporcionando um aumento de desempenho. 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 Python O próprio intérprete como recurso.

Por exemplo, suponha que você tenha escrito um Python Programa que utiliza duas threads para realizar operações de CPU e de entrada/saída (I/O). Ao executar este programa, ocorre o seguinte:

  1. O Python O interpretador cria um novo processo e inicia as threads.
  2. Quando o thread-1 começar a ser executado, ele primeiro adquirirá o GIL e o bloqueará.
  3. Se o thread-2 quiser executar agora, ele terá que esperar a liberação do GIL, mesmo que outro processador esteja livre.
  4. 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.
  5. 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 uma thread pode acessar o interpretador por vez, o que significa que haverá apenas uma thread em execução. Python código em um determinado ponto no tempo.

Isso funciona bem em um processador de núcleo único, pois ele usaria fatiamento de tempo (veja a primeira seção deste tutorial) para lidar com as threads. No entanto, no caso de processadores multi-core, uma função com uso intensivo de CPU executada em múltiplas threads terá um impacto considerável na eficiência do programa, já que ele não estará usando todos os núcleos disponíveis simultaneamente.

Por que o GIL foi necessário?

O CPython O coletor de lixo utiliza uma técnica eficiente de gerenciamento de memória conhecida como contagem de referências. Veja como funciona: Cada objeto em Python Um objeto possui uma contagem de referências, que é incrementada quando ele é atribuído a um novo nome de variável ou adicionado a um contêiner (como tuplas, listas, etc.). Da mesma forma, a contagem de referências é decrementada quando a referência sai do escopo ou quando a instrução `del` é chamada. Quando a contagem de referências de um objeto chega a 0, ele é coletado pelo coletor de lixo e a memória alocada é liberada.

Mas o problema é que a variável de contagem de referências está sujeita a condições de corrida, como qualquer outra variável global. Para resolver esse problema, os desenvolvedores de Python Decidimos usar o bloqueio global do interpretador. A outra opção seria adicionar um bloqueio a cada objeto, o que resultaria em impasses e aumentaria a sobrecarga das chamadas acquire() e release().

Portanto, o GIL é uma restrição significativa para multithreading. Python Programas que executam operações intensivas em CPU (tornando-os efetivamente de thread única). Se você deseja utilizar vários núcleos de CPU em seu aplicativo, use o multiprocessamento módulo em vez disso.

Perguntas Frequentes

Use threads para tarefas com uso intensivo de E/S, como chamadas de rede, acesso a arquivos ou consultas a bancos de dados, onde as threads aguardam recursos externos. Use multiprocessamento para tarefas com uso intensivo de CPU, como cálculos complexos, pois ele é executado em múltiplos núcleos e ignora o GIL (Global Interceptor Limit).

Não por padrão. A PEP 703 introduziu uma compilação opcional com suporte a multithreading em Python 3.13 que desativa o GIL, e Python A versão 3.14 dá continuidade a isso. As compilações padrão ainda incluem o GIL, então a maioria dos programas continua funcionando exatamente como antes.

Uma thread daemon é executada em segundo plano e não impede que o programa seja encerrado. Quando restam apenas threads daemon, Python Desliga-os e encerra. Defina um com thread.daemon = True antes de chamar start().

A classe Thread não retorna o resultado de run() diretamente. Armazene o valor no objeto thread ou em uma fila compartilhada, ou use concurrent.futures.ThreadPoolExecutor, cujo método submit() retorna um Future que você lê com result().

ThreadPoolExecutor é uma função auxiliar de alto nível que gerencia um conjunto de threads de trabalho para você. Você submete tarefas com submit() ou map(), e ela lida com a criação, reutilização e limpeza das threads, retornando objetos Future que armazenam cada resultado.

Você pode criar centenas ou milhares de threads, mas o GIL permite que apenas uma seja executada. Python bytecode em qualquer instante. Eles ainda se sobrepõem durante esperas de E/S, portanto, muitas threads simultâneas ajudam programas com uso intensivo de E/S muito mais do que programas com uso intensivo de CPU.

Sim, principalmente para etapas que exigem muita entrada/saída, como carregar dados, chamar APIs ou ler arquivos. Treinamentos complexos exigem muito da CPU, então bibliotecas como NumPy e Py são mais adequadas.TorPara obter paralelismo verdadeiro, libere o GIL em código C ou utilize multiprocessamento.

Sim. O GitHub Copilot completa automaticamente padrões comuns, como subclasses de Thread, uso de Lock e configurações de ThreadPoolExecutor, a partir de um comentário ou nome de função. Sempre revise o código gerado para verificar condições de corrida e garantir o tratamento correto de locks antes de utilizá-lo.

Resuma esta postagem com: