Multithreading em Python com exemplo: Aprenda GIL em Python

A linguagem de programação python permite usar multiprocessamento ou multithreading. Neste tutorial, você aprenderá como escrever aplicativos multithread 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 em 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 simultaneamenteneogeralmente. Esses processos não compartilham seus recursos e se comunicam através do IPC.

Multithreading Python vs 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 múltiplas subtarefas e executar essas tarefas simultaneamenteneogeralmente. Se você usar o multithreading corretamente, a velocidade, o desempenho e a renderização do seu aplicativo poderão ser melhorados.

MultiThreading em Python

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 lidar com threads em Python:

  1. A fio módulo, e
  2. 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. A partir do Python 3, ele foi considerado obsoleto e só pode ser acessado 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 seguintewing:

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 de 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 o módulo de tempo e thread que são usados ​​para lidar com a execução e atraso dos threads Python.
  2. 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.
  3. 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.

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 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.

Então, ao escrever um aplicativo multithread, você fará o seguintewing:

  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() 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 seguintewing:

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 o módulo de tempo e thread que é usado para lidar com a execução e atrasos dos threads Python.
  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 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().
  3. 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 ).
  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 um 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. Como resultado, se mais de um thread tentar alterar ou acessar os dados simultaneamenteneoGeralmente, erros podem surgir.

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.

Deadlockse condições de corrida

Antes de aprender sobre deadlockse 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 um atomtransação IC.
  • 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.

Deadlocks

Deadlocks são o problema mais temido que os desenvolvedores enfrentam ao escrever aplicativos simultâneos/multithread em python. A melhor maneira de entender deadlocks é usar o problema clássico de exemplo da ciência da computação conhecido como Para Refeições PhiloProblema de Sopher.

A declaração do problema para jantar philosophers é o seguinte:

Cinco philosophers estão sentados em uma mesa redonda com cinco pratos de espaguete (um tipo de macarrão) e cinco garfos, conforme mostrado no diagrama.

Para Refeições PhiloProblema de Sophers

Para Refeições PhiloProblema de Sophers

Em qualquer momento, um philoSopher deve estar comendo ou pensando.

Além disso, um philoSopher deve pegar os dois garfos adjacentes a ele (ou seja, os garfos esquerdo e direito) antes de poder comer o espaguete. O problema deadlock ocorre quando todos os cinco philosophers pegam seus garfos certos simultaneamenteneogeralmente.

Já que cada um dos philoSophers tiver um garfo, todos esperarão que os outros coloquem o garfo na mesa. Como resultado, nenhum deles poderá comer espaguete.

Da mesma forma, em um sistema concorrente, um deadlock ocorre quando diferentes threads ou processos (philosophers) 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.neogeralmente. 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.

Sincronizando threads

Para lidar com condições de corrida, deadlocks 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 atomic, 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:

  1. 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.
  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 uso de bloqueios em seus aplicativos. Acenda seu IDLE e digite o seguintewing:

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:

Sincronizando Threads

EXPLICAÇÃO DO CÓDIGO

Sincronizando 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 é 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:

  1. RLocks
  2. Semáforos
  3. Condições
  4. Eventos, e
  5. Barreiras

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

Antes de entrar no details do GIL do python, vamos definir alguns termos que serão úteis para a compreensão da próxima seção:

  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 através do sistema operacional
  3. CPython: é a referência implementação do Python e pode ser descrito como o intérprete escrito em C e Python (linguagem de programação).

O que é GIL em 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:

  1. O interpretador python cria um novo processo e gera os 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 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 coletor de lixo CPython 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ências, que aumenta quando é 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 diminui 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 teria resultado emadlockse aumentou a sobrecarga das chamadas adquirir() e release().

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:
    1. __fio módulo: fornece uma implementação de baixo nível para threading e é obsoleto.
    2. 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 seguintewing:
    1. Crie uma classe que estenda o Fio classe.
    2. Substitua seu construtor (__init__).
    3. Substituir seu corre() método.
    4. 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.
  • Isso pode ser evitado sincronizando threads.
  • Python oferece suporte a 6 maneiras de sincronizar threads:
    1. Locks
    2. RLocks
    3. Semáforos
    4. Condições
    5. Eventos, e
    6. 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:
    1. adquirir(): Define o estado de bloqueio para trancado. Se chamado em um objeto bloqueado, ele será bloqueado até que o recurso seja liberado.
    2. 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 processo do interpretador CPython pode ser executado por vez.
  • Foi usado para facilitar a funcionalidade de contagem de referências do coletor de lixo do CPythons.
  • Para criar aplicativos Python com operações pesadas vinculadas à CPU, você deve usar o módulo multiprocessamento.