How-To's Tutorials Web Development

Python mais rápido: concorrência com async/await e threading

Read this post in other languages:

Se você já programa em Python há algum tempo, especialmente se utiliza frameworks e bibliotecas como FastAPI e discord.py, provavelmente já utilizou async/await ou asyncio. Você pode ter ouvido afirmações como “multithreading em Python não é real” e também talvez conheça o famoso (ou indesejável) GIL em Python. Considerando a inexistência de multithreading em Python, você pode estar se perguntando qual é a diferença entre async/await e multithreading, especialmente na programação em Python. Se sim, este é o artigo ideal para você!

O que é multithreading?

Em programação, multithreading refere-se à capacidade de um programa executar várias tarefas sequenciais (chamadas threads) simultaneamente. Esses threads podem ser executados em um único núcleo do processador ou em vários núcleos. No entanto, devido à limitação do Global Interpreter Lock (GIL), o multithreading em Python é processado apenas em um único núcleo. A exceção é o Python nogil (também chamado de thread-free), que remove o GIL e será abordado na parte 2 desta série. Para esta publicação no blog, assumiremos que o GIL está sempre presente.

O que é concorrência?

Concorrência em programação significa que o computador está realizando mais de uma tarefa ao mesmo tempo, ou parece estar realizando mais de uma tarefa ao mesmo tempo, mesmo que as diferentes tarefas sejam executadas em um único processador. Ao gerenciar recursos e interações entre diferentes partes de um programa, diferentes tarefas podem progredir de forma independente e em intervalos de tempo sobrepostos.

Tanto asyncio quanto threading comportam-se de forma concorrente em Python

De maneira geral, as bibliotecas Python asyncio e threading permitem a concorrência. No entanto, suas CPUs não estão realizando várias tarefas simultaneamente. Apenas parece que sim.

Imagine que você está oferecendo um jantar com vários pratos para alguns convidados. Alguns pratos demoram mais tempo para serem preparados, como a torta que precisa ser assada no forno ou a sopa que precisa ser cozida no fogão. Enquanto aguardamos que cozinhem, não ficamos parados de braços cruzados. Faremos outras coisas nesse meio-tempo. Isso é semelhante à concorrência em Python. Às vezes, seu processo Python fica aguardando que algo seja concluído. Por exemplo, alguns processos de entrada/saída (E/S) estão sendo processados pelo sistema operacional e, nesse momento, o processo Python está apenas aguardando. Podemos então utilizar async para permitir que outro processo Python seja executado enquanto aguarda.

Multithreading x asyncio no Python

A diferença está em quem está no comando

Se tanto asyncio quanto threading parecem ser concorrentes, qual é a diferença entre eles? Bem, a principal diferença é uma questão de quem é responsável por qual processo está sendo executado e quando. Para async/await, a abordagem é às vezes chamada de concorrência cooperativa. Uma corrotina ou futuro cede seu controle a outra corrotina ou futuro para permitir que outros tenham uma oportunidade. Por outro lado, em threading, o gerenciador do sistema operacional controlará qual processo está em execução.

A concorrência cooperativa é como uma reunião em que um microfone é passado de pessoa para pessoa para que cada uma possa falar. Quem estiver com o microfone pode falar e, quando terminar ou não tiver mais nada a dizer, deve passar o microfone para a próxima pessoa. Em contraste, multithreading é uma reunião em que há um presidente que determina quem tem a palavra em um determinado momento. 

Escrevendo código simultâneo em Python

Vamos examinar como a concorrência funciona em Python, escrevendo alguns exemplos de código. Criaremos uma simulação de restaurante fast food utilizando tanto asyncio quanto threading.

Como funciona async/await em Python

O pacote asyncio foi introduzido no Python 3.4, enquanto as palavras-chave async e await foram introduzidas no Python 3.5. Um dos principais fatores que possibilitam o uso de async/await é o uso de corrotinas. Corrotinas em Python são, na verdade, geradores reaproveitados para poder pausar e retornar à função principal.

Agora, imagine uma hamburgueria onde apenas um funcionário está trabalhando. Os pedidos são preparados de acordo com uma fila do tipo primeiro a entrar, primeiro a sair, e nenhuma operação assíncrona pode ser realizada:

import time


def make_burger(order_num):
    print(f"Preparing burger #{order_num}...")
    time.sleep(5) # time for making the burger
    print(f"Burger made #{order_num}")


def main():
    for i in range(3):
        make_burger(i)


if __name__ == "__main__":
    s = time.perf_counter()
    main()
    elapsed = time.perf_counter() - s
    print(f"Orders completed in {elapsed:0.2f} seconds.")

Isso levará algum tempo para ser concluído:

Preparing burger #0...

Burger made #0

Preparing burger #1...

Burger made #1

Preparing burger #2...

Burger made #2

Orders completed in 15.01 seconds.

Agora, imagine que o restaurante contrata mais funcionários para que o trabalho possa ser realizado simultaneamente:

import asyncio

import time

async def make_burger(order_num):

    print(f"Preparing burger #{order_num}...")

    await asyncio.sleep(5) # time for making the burger

    print(f"Burger made #{order_num}")

async def main():

    order_queue = []

    for i in range(3):

        order_queue.append(make_burger(i))

    await asyncio.gather(*(order_queue))

if __name__ == "__main__":

    s = time.perf_counter()

    asyncio.run(main())

    elapsed = time.perf_counter() - s

    print(f"Orders completed in {elapsed:0.2f} seconds.")

Observamos a diferença entre os dois:

Preparing burger #0...

Preparing burger #1...

Preparing burger #2...

Burger made #0

Burger made #1

Burger made #2

Orders completed in 5.00 seconds.

Utilizando as funções fornecidas por asyncio, como run e gather, e as palavras-chave async e await, criamos corrotinas que podem preparar hambúrgueres simultaneamente.

Agora, vamos dar um passo adiante e criar uma simulação mais complexa. Imagine que temos apenas dois funcionários e que podemos preparar apenas dois hambúrgueres por vez.

import asyncio

import time

order_queue = asyncio.Queue()

def take_order():

  for i in range(3):

      order_queue.put_nowait(make_burger(i))

async def make_burger(order_num):

  print(f"Preparing burger #{order_num}...")

  await asyncio.sleep(5)  # time for making the burger

  print(f"Burger made #{order_num}")

class Staff:

  def __init__(self, name):

      self.name = name

  async def working(self):

      while order_queue.qsize() > 0:

          print(f"{self.name} is working...")

          task = await order_queue.get()

          await task

          print(f"{self.name} finished a task...")

async def main():

  staff1 = Staff(name="John")

  staff2 = Staff(name="Jane")

  take_order()

  await asyncio.gather(staff1.working(), staff2.working())

if __name__ == "__main__":

  s = time.perf_counter()

  asyncio.run(main())

  elapsed = time.perf_counter() - s

  print(f"Orders completed in {elapsed:0.2f} seconds.")

Aqui, utilizaremos uma fila para armazenar as tarefas, e os funcionários irão buscá-las.

John is working...

Preparing burger #0...

Jane is working...

Preparing burger #1...

Burger made #0

John finished a task...

John is working...

Preparing burger #2...

Burger made #1

Jane finished a task...

Burger made #2

John finished a task...

Orders completed in 10.00 seconds.

Neste exemplo, utilizamos asyncio.Queue para armazenar as tarefas, mas será mais útil se tivermos vários tipos de tarefas, conforme demonstrado no exemplo a seguir.

import asyncio

import time

task_queue = asyncio.Queue()

order_num = 0

async def take_order():

   global order_num

   order_num += 1

   print(f"Order burger and fries for order #{order_num:04d}:")

   burger_num = input("Number of burgers:")

   for i in range(int(burger_num)):

       await task_queue.put(make_burger(f"{order_num:04d}-burger{i:02d}"))

   fries_num = input("Number of fries:")

   for i in range(int(fries_num)):

       await task_queue.put(make_fries(f"{order_num:04d}-fries{i:02d}"))

   print(f"Order #{order_num:04d} queued.")

   await task_queue.put(take_order())

async def make_burger(order_num):

   print(f"Preparing burger #{order_num}...")

   await asyncio.sleep(5)  # time for making the burger

   print(f"Burger made #{order_num}")

async def make_fries(order_num):

   print(f"Preparing fries #{order_num}...")

   await asyncio.sleep(2)  # time for making fries

   print(f"Fries made #{order_num}")

class Staff:

   def __init__(self, name):

       self.name = name

   async def working(self):

       while True:

           if task_queue.qsize() > 0:

               print(f"{self.name} is working...")

               task = await task_queue.get()

               await task

               print(f"{self.name} finish task...")

           else:

               await asyncio.sleep(1) #rest

async def main():

   task_queue.put_nowait(take_order())

   staff1 = Staff(name="John")

   staff2 = Staff(name="Jane")

   await asyncio.gather(staff1.working(), staff2.working())

if __name__ == "__main__":

   s = time.perf_counter()

   asyncio.run(main())

   elapsed = time.perf_counter() - s

   print(f"Orders completed in {elapsed:0.2f} seconds.")

Neste exemplo, há várias tarefas, incluindo fazer batatas fritas, que leva menos tempo, e anotar pedidos, que envolve obter informações do usuário. 

Observe que o programa interrompe a espera pela entrada do usuário e até mesmo os outros funcionários que não estão atendendo o pedido, param de trabalhar em segundo plano. Isso ocorre porque a função input não é assíncrona (async) e, portanto, não é aguardada (await). Lembre-se de que o controle no código assíncrono só é liberado quando é aguardado. Para corrigir isso, podemos substituir:

input("Number of burgers:")

Por 

await asyncio.to_thread(input, "Number of burgers:")

E fazemos o mesmo com as batatas fritas – consulte o código abaixo. Observe que agora o programa está em um loop infinito. Se precisarmos interromper, podemos deliberadamente encerrar o programa com uma entrada inválida.

import asyncio

import time

task_queue = asyncio.Queue()

order_num = 0

async def take_order():

   global order_num

   order_num += 1

   print(f"Order burger and fries for order #{order_num:04d}:")

   burger_num = await asyncio.to_thread(input, "Number of burgers:")

   for i in range(int(burger_num)):

       await task_queue.put(make_burger(f"{order_num:04d}-burger{i:02d}"))

   fries_num = await asyncio.to_thread(input, "Number of fries:")

   for i in range(int(fries_num)):

       await task_queue.put(make_fries(f"{order_num:04d}-fries{i:02d}"))

   print(f"Order #{order_num:04d} queued.")

   await task_queue.put(take_order())

async def make_burger(order_num):

   print(f"Preparing burger #{order_num}...")

   await asyncio.sleep(5)  # time for making the burger

   print(f"Burger made #{order_num}")

async def make_fries(order_num):

   print(f"Preparing fries #{order_num}...")

   await asyncio.sleep(2)  # time for making fries

   print(f"Fries made #{order_num}")

class Staff:

   def __init__(self, name):

       self.name = name

   async def working(self):

       while True:

           if task_queue.qsize() > 0:

               print(f"{self.name} is working...")

               task = await task_queue.get()

               await task

               print(f"{self.name} finish task...")

           else:

               await asyncio.sleep(1) #rest

async def main():

   task_queue.put_nowait(take_order())

   staff1 = Staff(name="John")

   staff2 = Staff(name="Jane")

   await asyncio.gather(staff1.working(), staff2.working())

if __name__ == "__main__":

   s = time.perf_counter()

   asyncio.run(main())

   elapsed = time.perf_counter() - s

   print(f"Orders completed in {elapsed:0.2f} seconds.")

Ao utilizar asyncio.to_thread, colocamos a função input em um thread separado (consulte esta referência). Observe, no entanto, que este truque apenas desbloqueia tarefas limitadas por E/S se o Python GIL estiver presente.

Ao executar o código acima, você também poderá observar que a E/S padrão no terminal está embaralhada. A E/S do usuário e o registro do que está ocorrendo devem ser separados. Podemos inserir o registro em um log para análise posterior. 

import asyncio

import logging

import time

logger = logging.getLogger(__name__)

logging.basicConfig(filename='pyburger.log', level=logging.INFO)

task_queue = asyncio.Queue()

order_num = 0

closing = False

async def take_order():

   global order_num, closing

   try:

       order_num += 1

       logger.info(f"Taking Order #{order_num:04d}...")

       print(f"Order burger and fries for order #{order_num:04d}:")

       burger_num = await asyncio.to_thread(input, "Number of burgers:")

       for i in range(int(burger_num)):

           await task_queue.put(make_burger(f"{order_num:04d}-burger{i:02d}"))

       fries_num = await asyncio.to_thread(input, "Number of fries:")

       for i in range(int(fries_num)):

           await task_queue.put(make_fries(f"{order_num:04d}-fries{i:02d}"))

       logger.info(f"Order #{order_num:04d} queued.")

       print(f"Order #{order_num:04d} queued, please wait.")

       await task_queue.put(take_order())

   except ValueError:

       print("Goodbye!")

       logger.info("Closing down... stop taking orders and finish all tasks.")

       closing = True

async def make_burger(order_num):

   logger.info(f"Preparing burger #{order_num}...")

   await asyncio.sleep(5)  # time for making the burger

   logger.info(f"Burger made #{order_num}")

async def make_fries(order_num):

   logger.info(f"Preparing fries #{order_num}...")

   await asyncio.sleep(2)  # time for making fries

   logger.info(f"Fries made #{order_num}")

class Staff:

   def __init__(self, name):

       self.name = name

   async def working(self):

       while True:

           if task_queue.qsize() > 0:

               logger.info(f"{self.name} is working...")

               task = await task_queue.get()

               await task

               task_queue.task_done()

               logger.info(f"{self.name} finish task.")

           elif closing:

               return

           else:

               await asyncio.sleep(1) #rest

async def main():

   global task_queue

   task_queue.put_nowait(take_order())

   staff1 = Staff(name="John")

   staff2 = Staff(name="Jane")

   print("Welcome to Pyburger!")

   logger.info("Ready for business!")

   await asyncio.gather(staff1.working(), staff2.working())

   logger.info("All tasks finished. Closing now.")

if __name__ == "__main__":

   s = time.perf_counter()

   asyncio.run(main())

   elapsed = time.perf_counter() - s

   logger.info(f"Orders completed in {elapsed:0.2f} seconds.")

Neste bloco de código final, registramos as informações da simulação em pyburger.log e reservamos o terminal para mensagens aos clientes. Também detectamos entradas inválidas durante o processo de pedido e alteramos um sinalizador closing para True se a entrada for inválida, presumindo que o usuário deseja sair. Quando o sinalizador closing for definido como True, o worker irá retornar, encerrando o loop infinito while da corrotina.

Como funciona o threading em Python?

No exemplo acima, colocamos uma tarefa vinculada a E/S em outro thread. Você pode se perguntar se podemos colocar todas as tarefas em threads separadas e deixar que elas executem simultaneamente. Vamos tentar utilizar threading em vez de asyncio.

Considere o código abaixo, onde criamos hambúrgueres simultaneamente, sem nenhuma limitação:

import asyncio

import time

async def make_burger(order_num):

    print(f"Preparing burger #{order_num}...")

    await asyncio.sleep(5) # time for making the burger

    print(f"Burger made #{order_num}")

async def main():

    order_queue = []

    for i in range(3):

        order_queue.append(make_burger(i))

    await asyncio.gather(*(order_queue))

if __name__ == "__main__":

    s = time.perf_counter()

    asyncio.run(main())

    elapsed = time.perf_counter() - s

    print(f"Orders completed in {elapsed:0.2f} seconds.")

```

Instead of creating async coroutines to make the burgers, we can just send functions down different threads like this:

```

import threading

import time

def make_burger(order_num):

   print(f"Preparing burger #{order_num}...")

   time.sleep(5) # time for making the burger

   print(f"Burger made #{order_num}")

def main():

   order_queue = []

   for i in range(3):

       task = threading.Thread(target=make_burger, args=(i,))

       order_queue.append(task)

       task.start()

   for task in order_queue:

       task.join()

if __name__ == "__main__":

   s = time.perf_counter()

   main()

   elapsed = time.perf_counter() - s

   print(f"Orders completed in {elapsed:0.2f} seconds.")

No primeiro loop for em main, as tarefas são criadas em diferentes threads e são iniciadas. O segundo loop for garante que todos os hambúrgueres sejam preparados antes que o programa prossiga (ou seja, antes de retornar a main).

É mais complexo quando contamos com apenas dois funcionários. Cada membro da equipe é representado por um tópico e eles receberão tarefas de uma lista normal onde todas estão armazenadas.

import threading

import time

order_queue = []

def take_order():

   for i in range(3):

       order_queue.append(make_burger(i))

def make_burger(order_num):

   def making_burger():

       print(f"Preparing burger #{order_num}...")

       time.sleep(5)  # time for making the burger

       print(f"Burger made #{order_num}")

   return making_burger

def working():

     while len(order_queue) > 0:

         print(f"{threading.current_thread().name} is working...")

         task = order_queue.pop(0)

         task()

         print(f"{threading.current_thread().name} finish task...")

def main():

   take_order()

   staff1 = threading.Thread(target=working, name="John")

   staff1.start()

   staff2 = threading.Thread(target=working, name="Jane")

   staff2.start()

   staff1.join()

   staff2.join()

if __name__ == "__main__":

 s = time.perf_counter()

 main()

 elapsed = time.perf_counter() - s

 print(f"Orders completed in {elapsed:0.2f} seconds.")

Ao executar o código acima, pode ocorrer um erro em um dos threads, informando que está tentando obter uma tarefa de uma lista vazia. Você pode se perguntar por que isso ocorre, uma vez que temos uma condição no loop while que faz com que ele continue apenas se o task_queue não estiver vazio. No entanto, ainda recebemos um erro porque encontramos condições de corrida.

Condições de corrida

Condições de corrida podem ocorrer quando várias threads tentam acessar o mesmo recurso ou dados ao mesmo tempo e causam problemas no sistema. O momento e a ordem em que o recurso é acessado são importantes para a lógica do programa, e tempos imprevisíveis ou o entrelaçamento de várias threads acessando e modificando dados compartilhados podem causar erros.

Para resolver a condição de corrida em nosso programa, implementaremos um bloqueio no task_queue:

queue_lock = threading.Lock()

Para trabalhar, precisamos garantir que temos direitos de acesso à fila ao verificar seu comprimento e obter tarefas dela. Enquanto possuímos os direitos, outros threads não poderão acessar a fila:

def working():

   while True:

       with queue_lock:

           if len(order_queue) == 0:

               return

           else:

               task = order_queue.pop(0)

       print(f"{threading.current_thread().name} is working...")

       task()

       print(f"{threading.current_thread().name} finish task...")

```

Based on what we have learned so far, we can complete our final code with threading like this:

```

import logging

import threading

import time

logger = logging.getLogger(__name__)

logging.basicConfig(filename="pyburger_threads.log", level=logging.INFO)

queue_lock = threading.Lock()

task_queue = []

order_num = 0

closing = False

def take_order():

   global order_num, closing

   try:

       order_num += 1

       logger.info(f"Taking Order #{order_num:04d}...")

       print(f"Order burger and fries for order #{order_num:04d}:")

       burger_num = input("Number of burgers:")

       for i in range(int(burger_num)):

           with queue_lock:

               task_queue.append(make_burger(f"{order_num:04d}-burger{i:02d}"))

       fries_num = input("Number of fries:")

       for i in range(int(fries_num)):

           with queue_lock:

               task_queue.append(make_fries(f"{order_num:04d}-fries{i:02d}"))

       logger.info(f"Order #{order_num:04d} queued.")

       print(f"Order #{order_num:04d} queued, please wait.")

       with queue_lock:

           task_queue.append(take_order)

   except ValueError:

       print("Goodbye!")

       logger.info("Closing down... stop taking orders and finish all tasks.")

       closing = True

def make_burger(order_num):

   def making_burger():

       logger.info(f"Preparing burger #{order_num}...")

       time.sleep(5)  # time for making the burger

       logger.info(f"Burger made #{order_num}")

   return making_burger

def make_fries(order_num):

   def making_fries():

       logger.info(f"Preparing fried #{order_num}...")

       time.sleep(2)  # time for making fries

       logger.info(f"Fries made #{order_num}")

   return making_fries

def working():

   while True:

       with queue_lock:

           if len(task_queue) == 0:

               if closing:

                   return

               else:

                   task = None

           else:

               task = task_queue.pop(0)

       if task:

           logger.info(f"{threading.current_thread().name} is working...")

           task()

           logger.info(f"{threading.current_thread().name} finish task...")

       else:

           time.sleep(1)  # rest

def main():

   print("Welcome to Pyburger!")

   logger.info("Ready for business!")

   task_queue.append(take_order)

   staff1 = threading.Thread(target=working, name="John")

   staff1.start()

   staff2 = threading.Thread(target=working, name="Jane")

   staff2.start()

   staff1.join()

   staff2.join()

   logger.info("All tasks finished. Closing now.")

if __name__ == "__main__":

   s = time.perf_counter()

   main()

   elapsed = time.perf_counter() - s

   logger.info(f"Orders completed in {elapsed:0.2f} seconds.")

Ao comparar os dois trechos de código utilizando asyncio e threading, os resultados devem ser semelhantes. Você pode estar se perguntando qual é o melhor e por que deve escolher um em detrimento do outro.

Na prática, escrever código asyncio é mais fácil do que multithreading, pois não precisamos nos preocupar com possíveis condições de corrida e deadlocks. Os controles são transmitidos entre corrotinas por padrão, portanto, não são necessários bloqueios. No entanto, os threads Python têm o potencial de ser executados em paralelo, mas não na maioria das vezes com o GIL em vigor. Podemos revisitar este assunto quando discutirmos Python nogil (sem threads) na próxima postagem do blog.

Beneficiando-se da concorrência

Por que desejamos utilizar concorrência na programação? Há uma razão principal: a velocidade. Como ilustramos acima, as tarefas podem ser concluídas mais rapidamente se conseguirmos reduzir o tempo de espera. Existem diferentes tipos de espera na computação e, para cada um deles, tendemos a utilizar diferentes métodos para economizar tempo.

Tarefas vinculadas a E/S

Uma tarefa ou programa é considerada limitada por entrada/saída (E/S) quando sua velocidade de execução é limitada principalmente pela velocidade das operações de E/S, como leitura de um arquivo ou rede, ou espera pela entrada do usuário. As operações de E/S são geralmente mais lentas do que outras operações da CPU e, portanto, tarefas que envolvem muitas delas podem levar significativamente mais tempo. Exemplos típicos dessas tarefas incluem a leitura de dados de um banco de dados, o tratamento de solicitações da Web ou o trabalho com arquivos grandes.

O uso da concorrência async/await pode ajudar a otimizar o tempo de espera durante tarefas vinculadas a E/S, desbloqueando a sequência de processamento e permitindo que outras tarefas sejam realizadas enquanto se aguarda.

A concorrência com async/await é benéfica em muitos aplicativos Python, como aplicativos Web que envolvem muita comunicação com bancos de dados e tratamento de solicitações Web. As GUIs (interfaces gráficas do usuário) também podem se beneficiar da concorrência async/await, permitindo que tarefas em segundo plano sejam executadas enquanto o usuário interage com o aplicativo.

Tarefas vinculadas à CPU

Uma tarefa ou programa é considerada dependente da CPU quando sua velocidade de execução é limitada principalmente pela velocidade da CPU. Exemplos típicos incluem processamento de imagens ou vídeos, como redimensionamento ou edição, e cálculos matemáticos complexos, como multiplicação de matrizes ou treinamento de modelos de machine learning.

Ao contrário das tarefas vinculadas a E/S, as tarefas vinculadas à CPU raramente podem ser otimizadas usando concorrência com async/await, pois a CPU já está ocupada trabalhando nas tarefas. Se você possui mais de uma CPU em sua máquina ou se pode transferir algumas dessas tarefas para uma ou mais GPUs, as tarefas vinculadas à CPU podem ser concluídas mais rapidamente criando mais threads e executando multiprocessamento. O multiprocessamento pode otimizar a forma como essas CPUs e GPUs são utilizadas, razão pela qual muitos modelos de machine learning e IA atualmente são treinados em várias GPUs.

No entanto, isso é difícil de realizar com código Python puro, pois o Python em si foi projetado para fornecer camadas abstratas, para que os usuários não precisem controlar os processos de computação de nível inferior. Além disso, o GIL do Python limita o compartilhamento de recursos Python entre várias threads no seu computador. Recentemente, o Python 3.13 possibilitou a remoção do GIL, permitindo o multithreading verdadeiro. Discutiremos o GIL e a possibilidade de prescindir dele na próxima publicação do blog.

Às vezes, nenhum dos métodos mencionados acima é capaz de acelerar suficientemente as tarefas vinculadas à CPU. Nesse caso, as tarefas vinculadas à CPU podem precisar ser divididas em tarefas menores para que possam ser executadas simultaneamente em vários threads, vários processadores ou até mesmo várias máquinas. Isso é processamento paralelo, e talvez seja necessário reescrever completamente o código para implementá-lo. Em Python, o pacote multiprocessing oferece concorrência local e remota, que pode ser usada para contornar a limitação do GIL. Também analisaremos alguns exemplos disso na próxima publicação do blog.

Depuração de código concorrente no PyCharm

A depuração de código assíncrono ou concorrente pode ser complexa, pois o programa não é executado em sequência, o que dificulta a identificação do local e do momento em que o código está sendo executado. Muitos desenvolvedores utilizam print para auxiliar no rastreamento do fluxo do código, mas essa abordagem não é recomendada, pois é muito desajeitada e não é fácil utilizá-la para investigar um programa complexo, como um programa concorrente. Além disso, é complicado arrumar tudo depois.

Muitos IDEs oferecem depuradores, que são excelentes para inspecionar variáveis e o fluxo do programa. Os depuradores também fornecem um rastreamento de pilha claro em múltiplos threads. Vamos verificar como podemos rastrear a task_queue da nossa simulação de restaurante de exemplo no PyCharm.

Primeiramente, definiremos alguns pontos de interrupção em nosso código. Você pode fazer isso clicando no número da linha onde deseja que o depurador pause. O número da linha se transformará em um ponto vermelho, indicando que um ponto de interrupção foi definido nesse local. Iremos inserir pontos de interrupção nas linhas 23, 27 e 65, onde task_queue é alterado em diferentes threads.

Em seguida, podemos executar o programa no modo de depuração clicando no pequeno ícone de besouro no canto superior direito.

Após clicar no ícone, a janela Debug será aberta. O programa será executado até atingir o primeiro ponto de interrupção destacado no código.

Aqui, observamos que o thread John está tentando selecionar a tarefa, e a linha 65 está realçada. Neste momento, a linha realçada ainda não foi executada. Isso é útil quando desejamos inspecionar as variáveis antes de inserir o ponto de interrupção.

Vamos verificar o que está em task_queue. Para isso, basta começar a digitar na janela Debug, conforme mostrado abaixo.

Selecione ou digite “task_queue” e pressione Enter. Você observará que a tarefa take_order está na fila.

Agora, vamos executar o ponto de interrupção clicando no botão Step in, conforme mostrado abaixo.

Após pressionar e inspecionar a janela Special Variables que aparece, observamos que a variável da tarefa agora é take_order na thread John.

Ao consultar novamente task_queue, observamos que agora a lista está vazia.

Agora, clique no botão Resume Program e deixe o programa ser executado.

Quando o programa chegar à parte de entrada do usuário, o PyCharm nos levará à janela Console para que possamos fornecer a entrada. Digamos que desejamos dois hambúrgueres. Digite “2” e pressione Enter.

Agora chegamos ao segundo ponto de interrupção. Se clicarmos em Threads & Variables para retornar à janela anterior, observaremos que burger_num é dois, conforme inserido.

Agora, vamos entrar no ponto de interrupção e inspecionar o task_queue, como fizemos anteriormente. Observamos que uma tarefa make_burger foi adicionada.

Executamos o programa novamente e, se entrarmos no ponto de interrupção quando ele parar, observamos que Jane está assumindo a tarefa.

É possível examinar o restante do código por conta própria. Quando terminar, basta pressionar o botão vermelho Stop na parte superior da janela.

Com o depurador no PyCharm, é possível acompanhar a execução do seu programa em diferentes threads e inspecionar diferentes variáveis com facilidade.


Conclusão

Agora que aprendemos os conceitos básicos de concorrência em Python, espero que você consiga dominá-los com a prática. Na próxima postagem do blog, examinaremos o GIL do Python, sua função e o que muda quando ele está ausente.

O PyCharm oferece ferramentas poderosas para trabalhar com código Python simultâneo. Conforme demonstrado nesta publicação do blog, o depurador permite a inspeção passo a passo de código assíncrono e com threads, auxiliando no rastreamento do fluxo de execução, no monitoramento de recursos compartilhados e na detecção de problemas. Com pontos de interrupção intuitivos, visualizações de variáveis em tempo real, integração perfeita com o console para entrada do usuário e suporte robusto a registros, o PyCharm facilita a escrita, o teste e a depuração de aplicativos com confiança e clareza.

Artigo original em inglês por:

Cheuk Ting Ho

Cheuk Ting Ho

image description

Discover more