Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Industrial programming >> python

Multithreading em Python com Exemplo:Aprenda GIL em Python


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?


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 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 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 de função ao mesmo tempo.

Neste tutorial, você aprenderá,

O que é multiprocessamento?


O multiprocessamento permite executar 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 processos e threads, considere este cenário:Um arquivo .exe em 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á sendo executada agora é chamada de processo.

Todo processo terá 2 componentes fundamentais:

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 independentemente 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?


O 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 de seu aplicativo poderão ser aprimorados.

Python MultiThreading


Python suporta construções tanto para multiprocessamento quanto para multithreading. Neste tutorial, você se concentrará principalmente na implementação de multithreaded aplicativos com python. Existem dois módulos principais que podem ser usados ​​para lidar com threads em Python:
  1. O tópico módulo e
  2. O encadeamento módulo

No entanto, em python, também existe algo chamado de bloqueio de intérprete global (GIL). Ele 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 encadeamento .

No entanto, o módulo de thread foi preterido há muito tempo. A partir do Python 3, ele foi designado como obsoleto e só pode ser acessado como __thread para compatibilidade com versões anteriores.

Você deve usar o encadeamento de nível superior módulo para aplicativos que você pretende implantar. O módulo de tópicos foi abordado aqui apenas 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. 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ê deve ver:



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


EXPLICAÇÃO DO CÓDIGO

  1. Essas instruções importam o módulo de tempo e encadeamento que são usados ​​para lidar com a execução e o atraso dos encadeamentos do Python.
  2. Aqui, você definiu uma função chamada thread_test, que será chamado pelo start_new_thread método. A função executa um loop while por quatro iterações e imprime o nome da thread que a chamou. Quando a iteração estiver concluída, ele imprime uma mensagem informando que o encadeamento terminou a execução.
  3. Esta é a seção principal do seu programa. Aqui, você simplesmente chama o start_new_thread método com o thread_test função como um 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) com qualquer outra função que você deseja executar como um encadeamento.

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 fornece 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 Thread objetos que ainda estão vivos
atualThread() Retorna o objeto atual da classe Thread.
enumerar() Lista todos os objetos Thread ativos.
isDaemon() Retorna true se o thread for um daemon.
isAlive() Retorna verdadeiro se o encadeamento ainda estiver ativo.
Métodos de classe de thread
iniciar() Inicia a atividade de um thread. Ele deve ser chamado apenas uma vez para cada thread porque lançará um erro de tempo de execução se for chamado várias vezes.
executar() Este método denota a atividade de um thread e pode ser substituído por uma classe que estende a classe Thread.
juntar() 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:A classe de tópicos


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 template e as operações de uma 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 separado de controle.

Portanto, ao escrever um aplicativo multithread, você fará o seguinte:
  1. defina uma classe que estende a classe Thread
  2. Substituir o __init__ construtor
  3. Substituir o run() método

Depois que um objeto de thread é criado, o start() pode ser usado para iniciar a execução desta atividade e o 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 encadeamento para implementar seu exemplo anterior. Novamente, inicie 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


  1. Esta parte é igual ao nosso exemplo anterior. Aqui, você importa o módulo de tempo e encadeamento que são usados ​​para lidar com a execução e os atrasos dos encadeamentos do Python.
  2. Neste bit, você está criando uma classe chamada threadtester, que herda ou estende o Thread classe do módulo de rosqueamento. Esta é uma das maneiras mais comuns de criar threads em python. No entanto, você só deve substituir o construtor e o run() método em seu aplicativo. Como você pode ver no exemplo de código acima, o __init__ método (construtor) foi substituído. Da mesma forma, você também substituiu o run() 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, diminui-o em 1 em cada iteração e percorre o resto do código até que i se torne 0. Em cada iteração, imprime o nome da linha de execução actualmente em execução e dorme durante os segundos de espera (o que também é tomado 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 encadeamento, o segundo parâmetro é o nome do encadeamento 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() de sua classe.
  6. 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, 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 aparecer quando as 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 deadlocks 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 realizada como uma transação atômica.
  • Mudança de contextoÉ o processo que uma CPU segue para armazenar o estado de uma thread antes de mudar de uma tarefa para outra, para que possa ser retomada do mesmo ponto posteriormente.

Impasses


Deadlocks 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 é usando o problema clássico de exemplo de ciência da computação conhecido como Problema dos Filósofos de Jantar.

A declaraçã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 macarrão) 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 (ou seja, os garfos esquerdo e direito) antes de poder comer o espaguete. O problema do impasse ocorre quando todos os cinco filósofos pegam seus garfos certos simultaneamente.

Como cada um dos filósofos tem um garfo, todos eles esperarão que os outros abaixe o garfo. Como resultado, nenhum deles poderá 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 (forks) ao mesmo tempo. Como resultado, nenhum dos processos tem a chance de ser executado, pois estão esperando por outro recurso retido 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 esse código de uma só 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 entre 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 tópicos


Para lidar com condições de corrida, impasses e outros problemas baseados em encadeamento, o módulo de encadeamento fornece o Bloqueio objeto. A ideia é que quando um thread deseja acessar um recurso específico, ele adquira um bloqueio para esse recurso. Uma vez que um encadeamento bloqueia um recurso específico, nenhum outro encadeamento 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 __thread módulo. A qualquer momento, um bloqueio pode estar em um dos dois estados:bloqueado ou desbloqueado. Ele suporta dois métodos:
  1. adquirir() Quando o estado de bloqueio é desbloqueado, chamar o método Acquir() mudará o estado para bloqueado e retornará. No entanto, se o estado estiver bloqueado, a chamada para Acquir() será bloqueada até que o método release() seja chamado por algum outro encadeamento.
  2. liberar() 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. Ative 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ê deve ver uma saída como esta:


EXPLICAÇÃO DO CÓDIGO


  1. Aqui, você está simplesmente criando um novo bloqueio chamando o método 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 Acquir(). Quando o bloqueio for concedido, você imprime “bloqueio adquirido” para o console. Quando todo o código que você deseja que o encadeamento execute tiver concluído a execução, você libera o bloqueio chamando o método release().

A teoria é boa, mas como você sabe que a fechadura realmente funcionou? Se você observar 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 eram aleatórias porque vários threads estavam acessando o método print() ao mesmo tempo. Aqui, a função de impressão é chamada somente depois que o bloqueio é adquirido. Assim, as saídas são exibidas uma de cada vez e linha por linha.

Além dos bloqueios, o python também suporta alguns outros mecanismos para lidar com a sincronização de threads, conforme listado abaixo:
  1. RLbloqueios
  2. Semáforos
  3. Condições
  4. Eventos e
  5. Barreiras

Bloqueio de intérprete global (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:
  1. Código vinculado à CPU:refere-se a qualquer pedaço 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 SO
  3. CPython:é a referência implementação de Python e pode ser descrito como o interpretador escrito em C e Python (linguagem de programação).

O que é GIL em Python?


Bloqueio de intérprete global (GIL) em python é um bloqueio de processo ou um mutex usado ao lidar com os processos. Ele garante que um thread possa acessar um determinado recurso de cada vez e também impede 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 todo 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 'E/S'. Quando você executa este programa, isso é o que acontece:
  1. O interpretador python cria um novo processo e gera os encadeamentos
  2. Quando o thread-1 começar a ser executado, ele primeiro adquirirá o GIL e o bloqueará.
  3. Se o thread-2 quiser ser executado agora, ele terá que esperar que o GIL seja liberado mesmo que outro processador esteja livre.
  4. Agora, suponha que o thread-1 esteja aguardando uma operação de E/S. Neste momento, ele liberará o GIL e o thread-2 o adquirirá.
  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 usaria o fatiamento de tempo (consulte a primeira seção deste tutorial) para lidar com os encadeamentos. No entanto, no caso de processadores com vários núcleos, uma função vinculada à CPU executada em vários threads terá um impacto considerável na eficiência do programa, pois na verdade ele não usará todos os núcleos disponíveis ao mesmo tempo.

Por que o GIL era 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:todo objeto em python tem uma contagem de referência, que é aumentada 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 é 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 é propensa 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 deadlocks e aumentaria 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. __thread module:fornece uma implementação de baixo nível para encadeamento e é obsoleto.
    2. módulo de encadeamento :fornece uma implementação de alto nível para multithreading e é o padrão atual.
  • Para criar um encadeamento usando o módulo de encadeamento, você deve fazer o seguinte:
    1. Crie uma classe que estenda o Thread aula.
    2. Substituir seu construtor (__init__).
    3. Substituir seu run() método.
    4. Crie um objeto desta classe.
  • Um encadeamento pode ser executado chamando o método start() método.
  • A junção() pode ser usado para bloquear outros threads até que este thread (aquele em que o join foi chamado) termine a execução.
  • Uma condição de corrida ocorre quando vários encadeamentos acessam ou modificam um recurso compartilhado ao mesmo tempo.
  • Isso pode ser evitado sincronizando os encadeamentos.
  • Python suporta 6 maneiras de sincronizar threads:
    1. Bloqueios
    2. RLbloqueios
    3. Semáforos
    4. Condições
    5. Eventos e
    6. Barreiras
  • Os bloqueios permitem que apenas um segmento específico que tenha adquirido o bloqueio entre na seção crítica.
  • Um bloqueio tem 2 métodos principais:
    1. adquirir() :Define o estado de bloqueio como bloqueado. Se chamado em um objeto bloqueado, ele bloqueia até que o recurso seja liberado.
    2. liberar() :define o estado de bloqueio como desbloqueado e retorna. Se chamado em um objeto desbloqueado, ele retornará false.
  • O bloqueio global do interpretador é um mecanismo pelo qual apenas 1 processo interpretador CPython pode ser executado por vez.
  • Ele foi usado para facilitar a funcionalidade de contagem de referência do coletor de lixo do CPythons.
  • Para criar aplicativos Python com operações pesadas vinculadas à CPU, você deve usar o módulo de multiprocessamento.

python

  1. Função free() na biblioteca C:Como usar? Aprenda com o Exemplo
  2. Função Python String strip () com EXEMPLO
  3. Python String count() com EXEMPLOS
  4. Função Python round() com EXEMPLOS
  5. Função Python map() com EXEMPLOS
  6. Python Timeit() com exemplos
  7. Contador Python em coleções com exemplo
  8. Python List count() com EXEMPLOS
  9. Python List index() com exemplo
  10. C# - Multithreading