A linguagem de programação python permite que você use multiprocessamento ou multithreading. Neste tutorial, você aprenderá como escrever aplicativos multithread em Python.
O que é um tópico?
Um thread é uma unidade de execução na programação simultânea. Multithreading é uma técnica que permite a uma CPU executar várias 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 em seu computador (como um navegador ou editor de texto), o sistema operacional cria um processo.
O que é multithreading em Python?
Multithreading na programação Python é 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 do 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 de função ao mesmo tempo.
O que é multiprocessamento?
O multiprocessamento permite que você execute vários processos não relacionados simultaneamente. Esses processos não compartilham seus recursos e se comunicam por meio do IPC.
Python Multithreading vs Multiprocessing
Para entender os 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 agora está em execução é chamada de processo.
Cada processo terá 2 componentes fundamentais:
- O código
- Os dados
Agora, um processo pode conter uma ou mais subpartes chamadas threads. 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, é 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.
Neste tutorial, você aprenderá,
- O que é um tópico?
- O que é um processo?
- O que é multithreading?
- O que é multiprocessamento?
- Python Multithreading vs Multiprocessing
- Por que usar multithreading?
- Python MultiThreading
- Os módulos Thread e Threading
- O Módulo Thread
- O Módulo de Threading
- Deadlocks e condições de corrida
- Sincronizando threads
- O que é GIL?
- Por que o GIL foi necessário?
Por que usar multithreading?
O multithreading permite que você divida um aplicativo em várias subtarefas e execute essas tarefas simultaneamente. Se você usar multithreading corretamente, a velocidade, o desempenho e a renderização de seu aplicativo podem ser melhorados.
Python MultiThreading
Python suporta construções tanto para multiprocessamento quanto para multithreading. Neste tutorial, você se concentrará principalmente na implementação de aplicativos multithread com python. Existem dois módulos principais que podem ser usados para lidar com threads em Python:
- O módulo thread , e
- O módulo de threading
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 o módulo de threading .
No entanto, o módulo thread está obsoleto há muito tempo. A partir do Python 3, ele foi designado como obsoleto e só está acessível como __thread para compatibilidade com versões anteriores.
Você deve usar o módulo de threading de nível superior para os aplicativos que pretende implementar. O módulo thread só foi abordado aqui para fins educacionais.
O Módulo 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. Portanto, abra seu IDLE ou um bloco de notas e digite o seguinte:
import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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ê deve 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 tempo e o módulo de thread que são usados para controlar a execução e o atraso dos threads do Python.
- Aqui, você definiu uma função chamada thread_test, que será chamada pelo método start_new_thread . A função executa um loop while por quatro iterações e imprime o nome da thread que a chamou. Assim que a iteração for concluída, ele imprime uma mensagem dizendo que a execução do thread terminou.
- Esta é a seção principal do seu programa. Aqui, você simplesmente chama o método start_new_thread com a função thread_test como um argumento.
Isso criará um novo thread para a função que você passou como argumento e começará a executá-la. Observe que você pode substituir este (thread _ test) por 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 gerenciar aplicativos multithread. Ele oferece uma ampla gama de recursos quando comparado ao módulo de rosca.
Aqui está uma lista de algumas funções úteis definidas neste módulo:
Nome da Função | Descrição |
activeCount () | Retorna a contagem de objetos Thread 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 encadeamento for um daemon. |
Está vivo() | Retorna verdadeiro se o segmento ainda estiver ativo. |
Métodos de classe de thread | |
começar() | Inicia a atividade de um tópico. Ele deve ser chamado apenas uma vez para cada thread, pois lançará 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 encadeamento 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 de 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 thread de controle separado .
Portanto, ao escrever um aplicativo multithread, você fará o seguinte:
- definir uma classe que estende a classe Thread
- Substitua o construtor __init__
- Substitua o método run ()
Uma vez que um objeto thread foi criado, o método start () pode ser usado para iniciar a execução desta atividade e o método join () pode ser usado para bloquear todos os outros códigos até que a atividade atual termine.
Agora, vamos tentar usar o módulo de threading para implementar seu exemplo anterior. Novamente, ligue seu IDLE e digite o seguinte:
import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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()
Este será o resultado quando você executar o código acima:
EXPLICAÇÃO DO CÓDIGO
- Esta parte é igual ao nosso exemplo anterior. Aqui, você importa o módulo de tempo e thread que são usados para lidar com a execução e atrasos dos threads do Python.
- Nesta parte, você está criando uma classe chamada threadtester, que herda ou estende a classe Thread do módulo de threading. Esta é uma das maneiras mais comuns de criar threads em python. No entanto, você só deve substituir o construtor e o método run () em seu aplicativo. Como você pode ver no exemplo de código acima, o método __init__ (construtor) foi substituído.
Da mesma forma, você também substituiu o método run () . 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 leva o valor de i como um argumento, diminui em 1 a cada iteração e faz um loop pelo resto 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 segundos de espera (que também é considerado um argumento).
- thread1 = threadtester (1, "Primeiro 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 uma thread. Internamente, a função start () chama o método run () de sua classe.
- thread3.join ()
O método join () bloqueia a execução de outro código e espera até que a thread em que foi chamado termine.
Como você já sabe, os 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, podem ocorrer erros.
Na próxima seção, você verá os diferentes tipos de complicações que podem aparecer quando os threads acessam os dados e a seção crítica sem verificar as transações de acesso existentes.
Deadlocks 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.
- Mudança de contexto
É o processo que uma CPU segue para armazenar o estado de um thread antes de mudar de uma tarefa para outra, de forma que possa ser retomado do mesmo ponto posteriormente.
Deadlocks
Deadlocks são o problema mais temido que os desenvolvedores enfrentam ao escrever aplicativos concorrentes / multithread em python. A melhor maneira de entender os impasses é usando o exemplo de problema clássico da ciência da computação conhecido como o problema dos filósofos de jantar.
A declaração do problema para os filósofos jantares é a seguinte:
Cinco filósofos estão sentados em uma mesa redonda com cinco pratos de espaguete (um tipo de massa) e cinco garfos, como 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 comer o espaguete. O problema do impasse ocorre quando todos os cinco filósofos pegam seus garfos direitos simultaneamente.
Como cada um dos filósofos tem um garfo, todos esperarão que os outros baixem o garfo. Como resultado, nenhum deles será capaz de comer espaguete.
Da mesma forma, em um sistema concorrente, um deadlock ocorre quando diferentes threads ou processos (filósofos) tentam adquirir os recursos compartilhados do sistema (bifurcações) ao mesmo tempo. Como resultado, nenhum dos processos tem chance de ser executado, pois estão esperando por 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 loop for:
i=0; # a global variablefor x in range(100):print(i)i+=1;
Se você criar um número n de threads que executam este código de uma vez, não poderá determinar o valor de i (que é compartilhado pelas threads) quando o programa terminar 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.
Essas 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, impasses e outros problemas baseados em thread, o módulo de threading fornece o objeto Lock . A ideia é que quando um thread deseja acessar um recurso específico, 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 módulo __thread . A qualquer momento, um bloqueio pode estar em um de 2 estados: bloqueado ou desbloqueado. Ele suporta dois métodos:
- adquirir()
Quando o estado de bloqueio é desbloqueado, chamar o método locate () altera o estado para bloqueado e retorna. No entanto, se o estado estiver bloqueado, a chamada para adquirir () será bloqueada até que o método release () seja chamado por alguma outra thread.
- lançamento()
O método release () é usado para definir o estado como desbloqueado, ou seja, para liberar um bloqueio. Ele pode ser chamado por qualquer thread, não necessariamente aquele que adquiriu o bloqueio.
Aqui está um exemplo de uso de bloqueios em seus aplicativos. Acione seu IDLE e digite o seguinte:
import threadinglock = 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, pressione F5. Você deve ver uma saída como esta:
EXPLICAÇÃO DO CÓDIGO
- Aqui, você está simplesmente criando um novo bloqueio chamando a função de fábrica threading.Lock () . Internamente, Lock () retorna uma instância da classe Lock concreta mais eficaz mantida pela plataforma.
- Na primeira instrução, você adquire o bloqueio chamando o método taking (). Quando o bloqueio for concedido, você imprime "bloqueio adquirido" no 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ê olhar a saída, verá que cada uma das instruções de impressão está imprimindo exatamente uma linha por vez. Lembre-se de que, em um exemplo anterior, as saídas de print foram aleatórias porque vários threads estavam acessando o método print () ao mesmo tempo. Aqui, a função de impressão é 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 bloqueios, o python também oferece suporte a alguns outros mecanismos para lidar com a sincronização de thread, conforme listado abaixo:
- RLocks
- Semáforos
- 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 entender a próxima seção:
- Código vinculado à CPU: refere-se a qualquer parte do 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 SO
- CPython: é a implementação de referência do Python e pode ser descrito como o interpretador escrito em C e Python (linguagem de programação).
O que é GIL em Python?
Global Interpreter Lock (GIL) em python é um bloqueio de processo ou um mutex usado ao lidar com os processos. Ele garante que um thread pode acessar um recurso específico por vez e também evita o uso de objetos e bytecodes ao mesmo tempo. 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.
Um dos recursos 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 realizar operações de CPU e 'I / O'. Quando você executa este programa, isto é o que acontece:
- O interpretador python cria um novo processo e gera os threads
- Quando o thread-1 começa a ser executado, ele primeiro adquire o GIL e o bloqueia.
- Se o thread-2 quiser ser executado agora, ele terá que esperar que o GIL seja liberado, mesmo se outro processador estiver livre.
- Agora, suponha que o thread-1 esteja esperando por 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 o código Python em um determinado momento.
Isso está certo em um processador de núcleo único porque usaria o fatiamento de tempo (consulte a primeira seção deste tutorial) para lidar com os threads. No entanto, no caso de processadores com vários núcleos, uma função vinculada à CPU em execução em vários threads terá um impacto considerável na eficiência do programa, uma vez que, na verdade, ele 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 de gerenciamento de memória eficiente conhecida como contagem de referência. Veja como funciona: Cada objeto em python tem uma contagem de referência, 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ência 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 é propensa a condições de corrida como qualquer outra variável global. Para resolver esse problema, os desenvolvedores do python decidiram usar o bloqueio de interpretador global. A outra opção era adicionar um bloqueio a cada objeto, o que teria resultado em bloqueios e aumento da sobrecarga das chamadas de aquisição () e liberação ().
Portanto, GIL é uma restrição significativa para programas python multithread que executam operações pesadas de CPU (tornando-os efetivamente single-threaded). Se você quiser usar vários núcleos de CPU em seu aplicativo, use o módulo de multiprocessamento .
Resumo
- Python suporta 2 módulos para multithreading:
- Módulo __thread : 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 de threading, você deve fazer o seguinte:
- Crie uma classe que estenda a classe Thread .
- Substitua seu construtor (__init__).
- Substitua seu método run () .
- Crie um objeto desta classe.
- Um thread pode ser executado chamando o método start () .
- O método join () 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:
- Fechaduras
- RLocks
- Semáforos
- Condições
- Eventos e
- Barreiras
- Os bloqueios permitem que apenas um determinado thread que adquiriu o bloqueio entre na seção crítica.
- Uma fechadura tem 2 métodos principais:
- adquirir () : Define o estado de bloqueio para bloqueado. Se chamado em um objeto bloqueado, ele bloqueia até que o recurso esteja livre.
- release () : define o estado de bloqueio para desbloqueado e retorna. Se chamado em um objeto desbloqueado, ele retorna falso.
- O bloqueio do interpretador global é um mecanismo através do qual apenas 1 processo de interpretador CPython pode ser executado por vez.
- Foi usado para facilitar a funcionalidade de contagem de referência do coletor de lixo do CPythons.
- Para fazer aplicativos Python com operações pesadas vinculadas à CPU, você deve usar o módulo de multiprocessamento.