O módulo `concurrent` em Python

O módulo `concurrent` em Python

Neste artigo, explicaremos o módulo concurrent em Python.

Ao esclarecer os conceitos de concorrência e paralelismo, explicaremos como implementar processamento assíncrono usando o módulo concurrent com exemplos práticos.

YouTube Video

O módulo concurrent em Python

Ao acelerar o processamento em Python, é importante estar atento às diferenças entre concorrência e paralelismo. O módulo concurrent é um meio importante de lidar com o processamento assíncrono de forma segura e simples, considerando essas diferenças.

A diferença entre concorrência e paralelismo

  • Concorrência significa projetar um processo para que múltiplas tarefas prossigam alternando entre elas em pequenas unidades de trabalho. Mesmo que as tarefas não estejam realmente rodando ao mesmo tempo, fazer uso dos "tempos de espera" permite tornar todo o processo mais eficiente.

  • Paralelismo é um mecanismo que executa fisicamente múltiplas tarefas ao mesmo tempo. Ao usar múltiplos núcleos de CPU, o processamento avança simultaneamente.

Ambas são técnicas para acelerar o processamento, mas concorrência é uma questão de design sobre 'como proceder', enquanto paralelismo é uma questão de execução sobre 'como rodar', tornando-as fundamentalmente diferentes.

O que é o módulo concurrent?

concurrent é uma biblioteca padrão do Python que fornece uma API de alto nível para lidar com concorrência e paralelismo de maneira segura e simples. Ela foi projetada para que você possa focar na 'execução das tarefas' sem ter que se preocupar com operações de baixo nível, como criar e gerenciar threads ou processos.

Papéis de ThreadPoolExecutor e ProcessPoolExecutor

O módulo concurrent oferece duas opções principais, dependendo da natureza da tarefa.

  • ThreadPoolExecutor Isso é adequado para implementações concorrentes, especialmente tarefas com muitos tempos de espera de I/O, como operações de rede ou de arquivos. Ao alternar entre tarefas, faz uso eficiente do tempo de espera.

  • ProcessPoolExecutor Esta implementação é voltada para processamento paralelo e é adequada para tarefas que exigem muito da CPU. Ele utiliza múltiplos processos para aproveitar ao máximo os núcleos de CPU disponíveis em paralelo.

Assim, uma característica principal do módulo concurrent é que ele fornece uma estrutura que permite escolher corretamente entre concorrência e paralelismo conforme necessário.

Noções básicas do ThreadPoolExecutor (para tarefas de I/O)

ThreadPoolExecutor é adequado para tarefas limitadas por I/O, como comunicação de rede e operações de arquivos. Ele distribui as tarefas por várias threads, aproveitando de forma eficiente o tempo de espera.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in futures:
13        print(future.result())
  • Neste exemplo, várias tarefas de I/O que aguardam um segundo são executadas concorrentemente. Ao usar submit, as chamadas de função são registradas como tarefas assíncronas e, ao chamar result(), você pode aguardar a conclusão e obter os resultados, permitindo implementar processamento concorrente que faz uso eficiente do tempo de espera de forma concisa.

Concorrência simples usando map

Se o controle complexo não for necessário, usar map pode tornar seu código mais conciso.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    results = executor.map(fetch_data, range(5))
11
12    for result in results:
13        print(result)
  • Neste exemplo, várias tarefas de I/O são executadas concorrentemente usando ThreadPoolExecutor.map. Como map retorna os resultados na mesma ordem dos dados de entrada, você pode escrever um código próximo ao processamento sequencial, e a execução concorrente é possível sem precisar se preocupar com o processamento assíncrono — isso é uma grande vantagem.

Noções básicas do ProcessPoolExecutor (para tarefas limitadas pela CPU)

Para cálculos pesados que utilizam totalmente a CPU, você deve usar processos em vez de threads. Isso permite evitar a limitação do Global Interpreter Lock (GIL).

 1from concurrent.futures import ProcessPoolExecutor
 2
 3def heavy_calculation(n):
 4    # Simulate a CPU-bound task
 5    total = 0
 6    for i in range(10_000_000):
 7        total += i * n
 8    return total
 9
10if __name__ == "__main__":
11    with ProcessPoolExecutor(max_workers=4) as executor:
12        results = executor.map(heavy_calculation, range(4))
13
14        for result in results:
15            print(result)

Neste exemplo, cálculos intensivos de CPU são executados em paralelo usando ProcessPoolExecutor. Como envolve a criação de processos, é necessário um guardião __main__, o que possibilita o processamento paralelo seguro utilizando múltiplos núcleos de CPU.

Processamento pela ordem de conclusão usando as_completed

as_completed é útil quando você deseja lidar com os resultados na ordem em que eles são concluídos.

 1from concurrent.futures import ThreadPoolExecutor, as_completed
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in as_completed(futures):
13        print(future.result())
  • Neste exemplo, várias tarefas assíncronas são executadas simultaneamente e os resultados são obtidos na ordem em que são concluídos. Usar as_completed permite lidar rapidamente com os resultados independentemente da ordem das tarefas, tornando-o adequado para exibição de progresso ou situações que requerem manipulação sequencial.

Tratamento de exceções

No concurrent, exceções são lançadas quando você chama result().

 1from concurrent.futures import ThreadPoolExecutor
 2
 3def risky_task(n):
 4    # Simulate a task that may fail for a specific input
 5    if n == 3:
 6        raise ValueError("Something went wrong")
 7    return n * 2
 8
 9with ThreadPoolExecutor() as executor:
10    futures = [executor.submit(risky_task, i) for i in range(5)]
11
12    for future in futures:
13        try:
14            print(future.result())
15        except Exception as e:
16            print("Error:", e)
  • Este exemplo demonstra que, mesmo que algumas tarefas gerem exceções, as outras continuam a execução e você pode tratar exceções individualmente ao obter os resultados. Ao usar Future do concurrent, é importante que sucessos e falhas no processamento assíncrono possam ser tratados com segurança.

Diretrizes para escolher entre threads e processos

Para usar concorrência e paralelismo de forma eficaz, é importante escolher a abordagem correta com base na natureza da tarefa.

Na prática, os seguintes critérios podem te ajudar a decidir.

  • Para processos com muitas esperas de E/S, como comunicação ou operações de arquivos, use ThreadPoolExecutor.
  • Para tarefas computacionais pesadas e limitadas pela CPU, use ProcessPoolExecutor.
  • Se houver muitas tarefas simples, usar map permite escrever um código mais conciso.
  • Se o controle preciso da ordem de execução ou tratamento de exceções for importante, combine submit com as_completed.

Benefícios de usar o concurrent

Ao usar o módulo concurrent, você pode lidar com o processamento assíncrono de forma segura e intuitiva.

Os principais benefícios são:.

  • Você não precisa se preocupar com o gerenciamento de threads ou processos em baixo nível.
  • Ele é fornecido como parte da biblioteca padrão do Python, para que você possa usá-lo com confiança.
  • O código se torna mais legível e fácil de manter.
  • É ideal como um primeiro passo para aprender sobre concorrência e paralelismo.

Apenas mantendo essas diretrizes em mente, você pode reduzir bastante os erros em implementações que usam concurrent.

Resumo

O módulo concurrent é a opção padrão para concorrência e paralelismo práticos em Python. Ele permite melhorar a performance sem alterar muito o conteúdo do seu processamento, o que é um benefício significativo na prática. Ao usar concurrent, você pode implementar processamento assíncrono de forma concisa enquanto gerencia com segurança o tratamento de exceções e o controle de execução.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video