Python assíncrono: várias formas de competição

Com o advento do Python 3, há um burburinho sobre "assincronismo" e "simultaneidade", podemos assumir que o Python introduziu recentemente esses recursos / conceitos. Mas isso não é verdade. Usamos essas operações muitas vezes. Além disso, os iniciantes podem pensar que o assíncrono é a única ou melhor maneira de recriar e usar operações assíncronas / paralelas. Neste artigo, veremos várias maneiras de obter o paralelismo, suas vantagens e desvantagens.

Definição dos termos:


Antes de nos aprofundarmos nos aspectos técnicos, é importante ter algum entendimento básico dos termos frequentemente usados ​​neste contexto.

Síncrono e assíncrono:

Em operações síncronas , as tarefas são executadas uma após a outra. Em tarefas assíncronas , podem ser iniciadas e concluídas independentemente uma da outra. Uma tarefa assíncrona pode iniciar e continuar em execução enquanto a execução é movida para uma nova tarefa. Tarefas assíncronas não bloqueiam (não force a espera pela conclusão da tarefa) e geralmente são executadas em segundo plano.

Por exemplo, você deve entrar em contato com uma agência de viagens para planejar suas próximas férias. Você precisa enviar uma carta ao seu supervisor antes de voar. No modo síncrono, você primeiro liga para a agência de viagens e, se for solicitado a esperar, esperará até que eles atendam. Então você começará a escrever uma carta para o líder. Assim, você completa as tarefas uma após a outra. [execução síncrona, aprox. tradutor] Mas, se você for esperto, eles pediram para você esperar [ aguarde o telefone, aprox. tradutor] você começará a escrever e-mails e, quando falar novamente, fará uma pausa na escrita, na conversa e depois adicionará a carta. Você também pode pedir a um amigo que ligue para a agência e escreva uma carta você mesmo. Isso é assincronia, as tarefas não se bloqueiam.

Competitividade e simultaneidade:

Competitividade implica que duas tarefas sejam executadas em conjunto . No exemplo anterior, quando consideramos o exemplo assíncrono, progredimos gradualmente escrevendo uma carta e depois conversando com um tour. agência. Isso é competitividade .

Quando pedimos para ligar para um amigo e escrevemos uma carta, as tarefas foram realizadas em paralelo .

A concorrência é essencialmente uma forma de competição. Mas a simultaneidade depende do hardware. Por exemplo, se a CPU tiver apenas um núcleo, duas tarefas não poderão ser executadas em paralelo. Eles simplesmente compartilham o tempo do processador entre si. Então isso é competição, mas não simultaneidade. Mas quando temos vários núcleos [como amigo no exemplo anterior, que é o segundo núcleo, aprox. tradutor] , podemos executar várias operações (dependendo do número de núcleos) ao mesmo tempo.

Para resumir:

  • Sincronização: bloqueia operações (bloqueio)
  • Assincronia: não bloqueia operações (sem bloqueio)
  • Competitividade: progresso conjunto (conjunto)
  • Simultaneidade: progresso paralelo (paralelo)

Concorrência implica concorrência. Mas a concorrência nem sempre implica simultaneidade.

Threads e processos


O Python suporta threads há muito tempo. Threads permitem que você execute operações de forma competitiva. Mas há um problema com o Global Interpreter Lock (GIL) devido ao qual os threads não puderam fornecer simultaneidade verdadeira. E, no entanto, com o advento do multiprocessamento, você pode usar vários núcleos usando Python.

Tópicos

Considere um pequeno exemplo. No código a seguir, a função worker será executada em vários threads de forma assíncrona e simultânea.

import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!") 

E aqui está um exemplo de saída:

 $ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds 

Assim, iniciamos 5 threads para colaboração e após o início (ou seja, após o lançamento da função de trabalho), a operação não espera que os threads sejam concluídos antes de passar para a próxima instrução de impressão. Esta é uma operação assíncrona.

Em nosso exemplo, passamos a função para o construtor Thread. Se quiséssemos, poderíamos implementar uma subclasse com um método (estilo OOP).

Leitura adicional:

Para saber mais sobre fluxos, use o link abaixo:


Bloqueio Global de Intérpretes (GIL)

O GIL foi introduzido para facilitar o manuseio de memória do CPython e fornecer a melhor integração com o C (por exemplo, com extensões). GIL é um mecanismo de bloqueio quando o interpretador Python executa apenas um thread por vez. I.e. somente um encadeamento pode ser executado no bytecode do Python por vez. O GIL garante que vários threads não sejam executados em paralelo .

Detalhes rápidos de GIL:

  • Um thread pode ser executado por vez.
  • O intérprete Python alterna entre threads para obter competitividade.
  • O GIL é aplicável ao CPython (implementação padrão). Mas, como, por exemplo, Jython e IronPython não possuem GIL.
  • O GIL torna os programas single-threaded mais rápidos.
  • O GIL geralmente não interfere na E / S.
  • O GIL facilita a integração de bibliotecas seguras para threads no C, graças ao GIL, temos muitas extensões / módulos de alto desempenho escritos em C.
  • Para tarefas dependentes da CPU, o intérprete verifica todos os N ticks e alterna os threads. Portanto, um segmento não bloqueia os outros.

Muitos vêem o GIL como fraqueza. Considero isso uma bênção, porque foram criadas bibliotecas como NumPy e SciPy, que ocupam uma posição especial e única na comunidade científica.

Leitura adicional:

Esses recursos permitirão que você mergulhe no GIL:


Processos

Para obter simultaneidade no Python, foi adicionado um módulo de multiprocessamento que fornece uma API e parece muito semelhante se você já usou o encadeamento .

Vamos apenas mudar o exemplo anterior. Agora a versão modificada usa o processo em vez do fluxo .

 import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!") 

O que mudou? Acabei de importar o módulo de multiprocessamento em vez de encadear . E então, em vez de um thread, usei um processo. Isso é tudo! Agora, em vez de muitos threads, usamos processos executados em diferentes núcleos da CPU (a menos, é claro, que seu processador tenha vários núcleos).

Usando a classe Pool, também podemos distribuir a execução de uma função entre vários processos para diferentes valores de entrada. Um exemplo dos documentos oficiais:

 from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3])) 

Aqui, em vez de iterar sobre a lista de valores e chamar a função f uma de cada vez, na verdade executamos a função em diferentes processos. Um processo executa f (1), o outro f (2) e o outro f (3). Finalmente, os resultados são novamente combinados em uma lista. Isso nos permite dividir cálculos pesados ​​em partes menores e executá-los em paralelo para cálculos mais rápidos.

Leitura adicional:


Módulo Concurrent.futures

O módulo concurrent.futures é grande e facilita a gravação de código assíncrono. Meus favoritos são ThreadPoolExecutor e ProcessPoolExecutor . Esses artistas suportam um conjunto de threads ou processos. Enviamos nossas tarefas para o pool e ele executa as tarefas em um encadeamento / processo acessível. Um objeto Future é retornado que pode ser usado para consultar e recuperar o resultado quando a tarefa for concluída.

E aqui está um exemplo ThreadPoolExecutor:

 from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result()) 

Tenho um artigo sobre concurrent.futures masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html . Pode ser útil para um estudo mais aprofundado deste módulo.

Leitura adicional:


Assíncio - o que, como e por quê?


Você provavelmente tem uma pergunta que muitas pessoas na comunidade Python têm - o que o asyncio traz de novo? Por que havia outra maneira de usar E / S assíncrona? Já não tínhamos threads e processos? Vamos ver!

Por que precisamos de assíncio?

Os processos são muito caros [em termos de consumo de recursos, aprox. tradutor] para criar. Portanto, para operações de E / S, os threads são selecionados principalmente. Sabemos que a E / S depende de coisas externas - unidades lentas ou atrasos desagradáveis ​​na rede tornam a E / S geralmente imprevisível. Agora, suponha que usamos threads para E / S. 3 threads executam várias tarefas de E / S. O intérprete teria que alternar entre fluxos competitivos e dar a cada um deles um tempo alternado. Chame os fluxos T1, T2 e T3. Três threads iniciaram sua operação de E / S. T3 o conclui primeiro. T2 e T1 ainda estão aguardando E / S. O intérprete Python está mudando para T1, mas ainda está aguardando. Bem, o intérprete se move para T2, e o intérprete ainda está aguardando, e depois se move para T3, que está pronto e executa o código. Você vê isso como um problema?

O T3 estava pronto, mas o intérprete trocou primeiro entre T2 e T1 - isso gerou custos de troca, que poderíamos ter evitado se o intérprete trocasse pela primeira vez para T3, certo?

O que é assínio?

O Asyncio nos fornece um loop de eventos junto com outras coisas interessantes. O loop de eventos monitora eventos de E / S e alterna tarefas que estão prontas e aguardando operações de E / S [loop de eventos é uma construção de software que aguarda a chegada e envia eventos ou mensagens no programa, aprox. tradutor] .

A ideia é muito simples. Há um loop de eventos. E temos funções que executam E / S assíncronas. Transferimos nossas funções para o loop de eventos e pedimos que ele as execute para nós. O loop de eventos nos retorna um objeto Future, como uma promessa de que, no futuro, obteremos algo. Nós nos apegamos a uma promessa, verificamos de tempos em tempos se isso importa (realmente não podemos esperar) e, finalmente, quando o valor é recebido, o usamos em algumas outras operações [ou seja, enviamos uma solicitação, recebemos um ticket imediatamente e nos disseram para esperar até o resultado chegar. Verificamos periodicamente o resultado e, assim que é recebido, pegamos um ingresso e obtemos um valor nele, aprox. tradutor] .

O Asyncio usa geradores e corotinas para interromper e retomar tarefas. Você pode ler os detalhes aqui:


Como usar asyncio?

Antes de começarmos, vamos dar uma olhada em um exemplo:

 import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever() 

Observe que a sintaxe async / waitit é apenas para Python 3.5 e posterior. Vamos analisar o código:

  • Temos uma função assíncrona display_date que usa um número (como identificador) e um loop de eventos como parâmetros.
  • A função possui um loop infinito, que é interrompido após 50 segundos. Mas durante esse período, ela imprime repetidamente o tempo e faz uma pausa. A função de espera pode aguardar a conclusão de outras funções assíncronas (corotina).
  • Passamos a função para o loop de eventos (usando o método sure_future).
  • Iniciamos um ciclo de eventos.

Sempre que o aguardar é chamado, o assíncio percebe que a função provavelmente levará algum tempo. Assim, ele interrompe a execução, começa a monitorar quaisquer eventos de E / S associados a ela e permite executar tarefas. Quando o assinante percebe que a E / S da função em pausa está pronta, ela retoma a função.

Fazendo a escolha certa.


Acabamos de passar pelas formas mais populares de competitividade. Mas a questão permanece - o que deve ser escolhido? Depende dos casos de uso. Pela minha experiência, tenho a tendência de seguir este pseudo-código:

 if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing") 

  • Limite da CPU => Multi Processamento
  • Limite de E / S, E / S rápida, número limitado de conexões => Multi Threading
  • E / S vinculada, E / S lenta, várias conexões => Assíncio

[Nota tradutor]

Source: https://habr.com/ru/post/pt421625/


All Articles