Trilha de aprendizado · Nível 13 · Tutorial 10

Integrar chamadas bloqueantes ao asyncio

Ao concluir, você será capaz de chamar funções síncronas a partir de corrotinas sem executá-las diretamente no laço de eventos, escolhendo o executor e o ciclo de vida adequados.

  • Nível: Avançado
  • Duração: 20 min
  • 8 passos
Integrar chamadas bloqueantes ao asyncio

O que você vai percorrer

  1. Delimitar a chamada que precisa sair do laço Reconheça onde uma chamada síncrona interrompe o laço de eventos e decida se uma API assíncrona nativa ou uma ponte de execução é a escolha adequada. 2 min
  2. Encaminhar entrada e saída com asyncio.to_thread Execute uma função síncrona de entrada e saída fora da thread do laço e receba o resultado sem interromper outras corrotinas. 3 min
  3. Integrar um executor explícito com run_in_executor Submeta funções síncronas a um ThreadPoolExecutor explícito e aguarde seus resultados sem bloquear o laço de eventos. 3 min
  4. Manter o encerramento bloqueante fora da corrotina Organize um executor explícito no escopo síncrono que envolve asyncio.run, mantendo a espera pelos resultados cooperativa dentro do laço. 3 min
  5. Usar processos sem mudar a forma de aguardar Encaminhe processamento para processos separados sem alterar a fronteira assíncrona: o resultado continua sendo obtido com await. 3 min
  6. Separar cancelamento da espera e término do trabalho Observe por que um timeout pode encerrar a espera assíncrona enquanto a função síncrona continua em execução. 3 min
  7. Preservar os recursos até o trabalhador terminar Organize recursos usados por funções síncronas de modo que um timeout assíncrono não os feche enquanto o trabalhador ainda estiver em execução. 2 min
  8. Aplicar a integração e verificar o ciclo completo Reúna as pontes estudadas em um script local: entrada e saída em thread, processamento em processo e timeout que encerra a espera sem necessariamente encerrar o trabalhador. 3 min

O que você vai aprender

  • Encaminhar uma função síncrona de entrada e saída para asyncio.to_thread.
  • Integrar um executor explícito ao laço com run_in_executor.
  • Evitar bloqueio do laço durante a obtenção de resultados e o encerramento de executores.
  • Explicar os limites de timeout e cancelamento quando o trabalho continua em outra thread ou processo.

Antes de começar

  • Aplicar tempos de espera a operações assíncronas
  • Executar funções em processos com ProcessPoolExecutor
  • Proteger estado compartilhado entre threads

Passo 1 de 8

Delimitar a chamada que precisa sair do laço

Reconheça onde uma chamada síncrona interrompe o laço de eventos e decida se uma API assíncrona nativa ou uma ponte de execução é a escolha adequada.

Primeiro, procure uma API assíncrona

A escolha preferencial

Em uma corrotina, prefira uma API assíncrona nativa quando ela estiver disponível e atender ao problema. Ela já foi projetada para suspender a tarefa durante a espera, permitindo que o laço coordene outras tarefas.

Se a única API necessária for síncrona e puder demorar — por exemplo, uma leitura, uma consulta ou uma chamada a serviço externo — não a execute diretamente na thread do laço. A chamada deve atravessar uma ponte de execução para um trabalhador; a corrotina continua responsável por coordenar e aguardar o resultado.

Dica

Delimite a fronteira

Não basta uma função estar sendo chamada dentro de async def: se ela faz espera síncrona, ela ainda pode bloquear o laço. Localize a operação concreta que demora, não apenas a corrotina que a contém.

O que muda para o laço

Chamada direta versus trabalho encaminhado

A diferença está em qual thread fica esperando pela operação síncrona.

Diagrama comparando dois fluxos. No primeiro, uma chamada síncrona de entrada e saída ocupa a thread do laço de eventos e impede uma tarefa periódica de avançar. No segundo, a chamada ocorre em um trabalhador separado enquanto o laço continua executando a tarefa periódica.

À esquerda, a espera síncrona paralisa o laço. À direita, o laço mantém a coordenação enquanto um trabalhador realiza a espera.

Exemplo

Sinal observável

Imagine uma corrotina que agenda uma tarefa para registrar um pulso periódico e, depois, precisa obter dados por uma biblioteca somente síncrona.

  • Se a obtenção de dados roda diretamente na thread do laço, os pulsos param até ela retornar.
  • Se essa obtenção é encaminhada a um trabalhador, os pulsos podem continuar aparecendo enquanto ela espera.

Os pulsos evidenciam que o laço continua progredindo. Eles não provam, por si só, que o programa ficou mais rápido: a operação ainda pode levar o mesmo tempo e ter outros custos.

Localize a chamada bloqueante

Qual chamada deve sair da thread do laço?

Considere que cliente.buscar_registro pertence a uma biblioteca somente síncrona e pode esperar por rede:

async def mostrar_registro(cliente, codigo):
    indicador = asyncio.create_task(mostrar_pulsos())
    registro = cliente.buscar_registro(codigo)
    await indicador
    return registro

Qual é a chamada que precisa ser encaminhada para fora da thread do laço?

Decida antes de integrar

Qual é a melhor escolha inicial?

Uma biblioteca oferece buscar_async(codigo), uma operação assíncrona que fornece os mesmos dados de que você precisa. Também existe a versão síncrona buscar(codigo). Qual opção é preferível dentro da corrotina?

Passo 2 de 8

Encaminhar entrada e saída com asyncio.to_thread

Execute uma função síncrona de entrada e saída fora da thread do laço e receba o resultado sem interromper outras corrotinas.

A ponte para uma função síncrona

Não chame a função no laço

Quando não houver uma API assíncrona adequada e você precisar usar uma operação síncrona de entrada e saída, use asyncio.to_thread. Ela recebe a função e seus argumentos, devolvendo uma corrotina aguardável.

aguardavel = asyncio.to_thread(funcao_sincrona, arg1, opcao=True)
resultado = await aguardavel

Passe funcao_sincrona, sem parênteses. Escrever funcao_sincrona() executaria a chamada imediatamente na thread do laço — justamente o que deve ser evitado.

A corrotina devolvida por to_thread começa a encaminhar o trabalho quando é aguardada ou quando é agendada como tarefa. O await entrega o valor retornado pela função ou propaga a exceção que ela produzir.

Quem continua trabalhando?

A ponte desloca a chamada síncrona; a coordenação das corrotinas permanece no laço de eventos.

Diagrama mostrando o laço de eventos mantendo uma tarefa periódica ativa enquanto uma operação de entrada e saída síncrona é executada em uma thread trabalhadora separada.

A tarefa periódica pode progredir enquanto a operação síncrona espera fora da thread do laço.

Atenção

Não é uma ponte para acelerar qualquer código

to_thread usa o executor padrão de threads; não há escolha de executor nessa chamada. Com o GIL habilitado, código Python limitado por processamento não ganha paralelismo de CPU apenas por usar threads. A ponte é especialmente útil para entrada e saída bloqueante — e também pode servir a extensões que liberam o GIL.

Prática local: espera sem bloquear o laço

Execute como um script local

Crie um arquivo, por exemplo ponte_io.py, cole o código completo abaixo e execute python ponte_io.py. A função síncrona usa time.sleep apenas para simular uma espera de entrada e saída finita; uma aplicação real chamaria uma API síncrona necessária.

Script completo

Observe que buscar_registro é passado sem parênteses e que tentativas=2 continua sendo um argumento nomeado da função síncrona.

python
import asyncio
import time


def buscar_registro(nome: str, *, tentativas: int) -> str:
    print(f"[trabalhadora] iniciando busca de {nome!r}")
    time.sleep(1.0)  # Simula uma espera de entrada e saída.
    print("[trabalhadora] busca concluída")
    return f"{nome}: obtido após {tentativas} tentativas"


async def mostrar_progresso() -> None:
    for numero in range(1, 5):
        await asyncio.sleep(0.25)
        print(f"[laço] pulso {numero}")


async def main() -> None:
    ponte = asyncio.to_thread(
        buscar_registro,
        "relatorio-mensal",
        tentativas=2,
    )

    tarefa_busca = asyncio.create_task(ponte)
    await mostrar_progresso()

    resultado = await tarefa_busca
    print(f"[laço] resultado: {resultado}")


asyncio.run(main())

Dica

O que observar

Enquanto a trabalhadora está na espera simulada, devem aparecer pulsos do laço. A ordem exata de algumas linhas pode variar, mas os pulsos não devem ficar todos represados até o fim da busca. Se buscar_registro lançasse uma exceção, o await tarefa_busca a propagaria para main.

Monte a chamada corretamente

Função, não resultado de função

Complete a lacuna para encaminhar a função sem executá-la no laço:

resultado = await asyncio.to_thread(
    _____,
    "relatorio-mensal",
    tentativas=2,
)

Verifique o efeito observado

Evidência de responsividade

Após executar o script, o que nos pulsos do terminal indica que a chamada síncrona não bloqueou o laço de eventos?

Escreva pelo menos 40 caracteres (0/40).

Passo 3 de 8

Integrar um executor explícito com run_in_executor

Submeta funções síncronas a um ThreadPoolExecutor explícito e aguarde seus resultados sem bloquear o laço de eventos.

Submissão imediata ao executor

O laço entrega o trabalho

Quando uma corrotina recebe um executor explícito, obtenha o laço que está em execução com asyncio.get_running_loop(). Em seguida, chame loop.run_in_executor(executor, funcao, *args).

Diferentemente de asyncio.to_thread, a chamada a run_in_executor já submete a função ao executor. Ela retorna um asyncio.Future, que pode ser aguardado com await.

Duas fronteiras de execução

A função síncrona sai da thread do laço; a corrotina continua coordenando a espera.

Diagrama comparando uma chamada síncrona direta que bloqueia o laço com run_in_executor, que envia a função a uma thread trabalhadora e devolve um futuro aguardável ao laço.

Com run_in_executor, a submissão ocorre na chamada e o resultado retorna por um futuro aguardável.

Dica

Executor explícito como dependência

Neste step, considere que o ThreadPoolExecutor já foi criado por quem chamou a corrotina e foi passado como argumento. A criação e o encerramento corretos desse executor serão organizados no próximo step.

Função, argumentos e resultado

Executar com argumentos posicionais

A função é passada sem parênteses; seus argumentos posicionais vêm depois dela.

python
import asyncio
from concurrent.futures import ThreadPoolExecutor


def ler_registro(codigo: int, tentativas: int) -> str:
    # Representa uma biblioteca síncrona de entrada e saída.
    return f"registro {codigo} lido em até {tentativas} tentativas"


async def consultar(executor: ThreadPoolExecutor) -> None:
    loop = asyncio.get_running_loop()

    futuro = loop.run_in_executor(executor, ler_registro, 42, 3)
    print("trabalho já foi submetido")

    resultado = await futuro
    print(resultado)

O que cada linha preserva

get_running_loop() só é usado durante a execução assíncrona. run_in_executor(...) encaminha ler_registro e os argumentos 42 e 3 sem chamar ler_registro na thread do laço.

await futuro suspende apenas esta corrotina enquanto o resultado não chega. Se ler_registro falhar, a exceção é relançada no ponto do await.

Atenção

Não chame a função antes de encaminhá-la

Evite loop.run_in_executor(executor, ler_registro(42, 3)). Nesse caso, ler_registro(42, 3) é executada imediatamente na thread do laço, e seu retorno — não a função — seria passado à API.

Argumentos nomeados e o futuro correto

Adaptar argumentos nomeados com partial

run_in_executor aceita argumentos posicionais após a função. Use partial para fixar argumentos nomeados antes da submissão.

python
import asyncio
from concurrent.futures import ThreadPoolExecutor
from functools import partial


def buscar_documento(codigo: int, *, idioma: str, incluir_anexos: bool) -> str:
    return f"documento {codigo}: {idioma}, anexos={incluir_anexos}"


async def consultar(executor: ThreadPoolExecutor) -> None:
    loop = asyncio.get_running_loop()

    chamada = partial(
        buscar_documento,
        42,
        idioma="pt-BR",
        incluir_anexos=False,
    )
    futuro = loop.run_in_executor(executor, chamada)
    print(await futuro)

Associe chamada, retorno e espera

Relacione cada expressão à sua característica principal.

Toque em um item e depois no par correspondente.

Aguardar sem parar o laço

Use await para receber o resultado

Dentro de uma corrotina, aguarde o retorno de run_in_executor com await futuro. Isso permite que o laço execute outras tarefas enquanto o trabalhador atua.

Não substitua essa espera por Future.result() de um futuro retornado por executor.submit: se o trabalho estiver pendente, essa chamada síncrona bloqueia a thread do laço.

Atenção

`.result()` não é uma espera assíncrona

No asyncio.Future devolvido por run_in_executor, .result() não espera: se ele ainda não terminou, ela levanta asyncio.InvalidStateError. Mesmo depois de concluído, prefira await no fluxo assíncrono: ele obtém o retorno e propaga a exceção no ponto correto.

Escolha a espera adequada

Após futuro = loop.run_in_executor(executor, ler_registro, 42, 3), qual linha obtém o resultado sem bloquear o laço enquanto o trabalho ainda está pendente?

Passo 4 de 8

Manter o encerramento bloqueante fora da corrotina

Organize um executor explícito no escopo síncrono que envolve asyncio.run, mantendo a espera pelos resultados cooperativa dentro do laço.

Dois escopos, duas responsabilidades

O executor pertence ao chamador

Quando você cria um executor explícito para usar com run_in_executor, o código síncrono que chama asyncio.run é responsável por seu ciclo de vida.

Crie o executor, entre no with, execute asyncio.run(principal(executor)) e só então saia do contexto. Dentro da corrotina, use await para receber os resultados; fora dela, a saída do with faz o encerramento síncrono do executor.

Fronteira correta do ciclo de vida

A moldura externa representa o trecho síncrono; o laço de eventos vive apenas durante asyncio.run.

Diagrama em camadas: um contexto síncrono de executor envolve asyncio.run; dentro dele há um laço de eventos que envia trabalho a threads e recebe resultados por await; o shutdown ocorre depois que asyncio.run termina.

O encerramento do executor acontece depois que o laço de eventos já terminou.

Atenção

Não feche o executor dentro da corrotina

Evite colocar with ThreadPoolExecutor(...) dentro de async def. Ao sair desse contexto, o executor pode executar shutdown(wait=True). Essa espera é síncrona e pode paralisar o laço, impedindo outras tarefas assíncronas de progredirem.

Script reorganizado

Passe o executor como dependência

Neste script, principal recebe o executor já criado. Ela aguarda cada futuro retornado por run_in_executor; falhas das funções síncronas são tratadas sem abandonar os demais resultados.

Executor externo a asyncio.run

Salve como um arquivo .py e execute localmente.

python
import asyncio
from concurrent.futures import ThreadPoolExecutor
import time


def consultar_codigo(codigo: int) -> str:
    time.sleep(0.2)  # Simula uma operação síncrona de entrada e saída.
    if codigo == 2:
        raise ValueError("código indisponível")
    return f"resultado do código {codigo}"


async def principal(executor: ThreadPoolExecutor) -> list[str]:
    loop = asyncio.get_running_loop()
    pendentes = [
        loop.run_in_executor(executor, consultar_codigo, codigo)
        for codigo in (1, 2, 3)
    ]

    resultados: list[str] = []
    for pendente in pendentes:
        try:
            resultados.append(await pendente)
        except ValueError as erro:
            resultados.append(f"falha tratada: {erro}")

    return resultados


def main() -> None:
    # O contexto é síncrono e envolve toda a vida do laço de eventos.
    with ThreadPoolExecutor(max_workers=2) as executor:
        resultados = asyncio.run(principal(executor))

    # Aqui asyncio.run terminou e o executor já foi encerrado.
    for resultado in resultados:
        print(resultado)


if __name__ == "__main__":
    main()

Dica

Compare as responsabilidades

await pendente não bloqueia a thread do laço enquanto o trabalhador executa. Já a saída do with é uma operação síncrona de encerramento; por isso ela fica em main(), depois de asyncio.run(...).

Confira a sequência

Ordem do ciclo de vida

Ordene as etapas de um programa que usa um ThreadPoolExecutor explícito com run_in_executor.

  1. Sair do contexto do executor, permitindo seu encerramento síncrono.
  2. Receber o retorno de asyncio.run no código síncrono.
  3. Chamar asyncio.run(principal(executor)) e aguardar os futuros com await dentro de principal.
  4. Entrar no contexto síncrono que cria o ThreadPoolExecutor.

Explique a escolha do escopo

Por que o with ThreadPoolExecutor(...) deve envolver asyncio.run(...), em vez de ficar dentro da corrotina?

Escreva pelo menos 80 caracteres (0/80).

Passo 5 de 8

Usar processos sem mudar a forma de aguardar

Encaminhe processamento para processos separados sem alterar a fronteira assíncrona: o resultado continua sendo obtido com await.

Processos atrás da mesma fronteira assíncrona

Troque o executor, mantenha o await

Quando uma função faz processamento intenso em código Python, um ProcessPoolExecutor pode executá-la em processos separados. Dentro da corrotina, a integração preserva a forma já usada com executor explícito:

resultado = await loop.run_in_executor(executor, funcao, argumento)

O executor muda de threads para processos; a forma de aguardar o resultado não muda. Processos podem ser uma escolha apropriada quando o GIL está habilitado, mas criar processos e transferir dados também tem custo. Portanto, isso não promete aceleração em todo caso.

A fronteira do await permanece

Compare o que muda e o que permanece na integração.

Diagrama mostrando uma corrotina no laço de eventos que chama run_in_executor e aguarda o resultado; o trabalho segue para dois processos separados e retorna à corrotina.

O laço coordena e aguarda; o processamento ocorre nos processos trabalhadores.

Estrutura portátil do script

Requisitos que continuam valendo

Use um arquivo .py local e execute-o com Python. A função enviada ao processo deve estar no nível superior do módulo; argumentos e retorno precisam ser serializáveis. Neste exemplo, a lista de inteiros é enviada ao trabalhador, que devolve outro inteiro.

O executor é criado no escopo síncrono, com contexto spawn, e envolve asyncio.run(). A proteção if __name__ == "__main__" impede que trabalhadores recriem o programa ao importar o módulo principal.

Script completo: processamento em processo

Salve como processar_com_asyncio.py e execute: python processar_com_asyncio.py

python
import asyncio
import multiprocessing
from concurrent.futures import ProcessPoolExecutor


def somar_quadrados(valores: list[int]) -> int:
    # Esta função fica no nível superior para poder ser importada pelo processo.
    return sum(valor * valor for valor in valores)


async def principal(executor: ProcessPoolExecutor) -> None:
    loop = asyncio.get_running_loop()
    valores = list(range(1, 100_001))

    futuro = loop.run_in_executor(executor, somar_quadrados, valores)
    resultado = await futuro

    print(f"Soma dos quadrados: {resultado}")


def executar() -> None:
    contexto = multiprocessing.get_context("spawn")
    with ProcessPoolExecutor(mp_context=contexto) as executor:
        asyncio.run(principal(executor))


if __name__ == "__main__":
    executar()

Dica

Leia a divisão de responsabilidades

run_in_executor(...) submete a função ao executor imediatamente. Já await futuro devolve o controle ao laço enquanto o processo trabalha e recebe o retorno ou a exceção quando a chamada termina.

Escolha a integração válida

Qual opção é portátil?

Qual alternativa descreve corretamente uma integração de processamento com ProcessPoolExecutor?

Justifique a escolha

Processamento, processos e await

Em duas ou três frases, explique por que o script usa ProcessPoolExecutor e por que ainda usa await para obter o resultado.

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 8

Separar cancelamento da espera e término do trabalho

Observe por que um timeout pode encerrar a espera assíncrona enquanto a função síncrona continua em execução.

Dois finais diferentes

O timeout encerra a espera, não necessariamente o trabalho

Quando uma corrotina aguarda uma função síncrona encaminhada a uma thread ou processo, há pelo menos dois eventos distintos:

  • fim da espera assíncrona: por resultado, timeout ou cancelamento;
  • fim real do trabalhador: quando a função síncrona retorna ou falha.

Se a função já começou, cancelar a tarefa que a aguardava não oferece uma interrupção forçada da função em execução. O mesmo cuidado vale para threads e processos.

Linha do tempo de uma espera limitada

O prazo observado pela corrotina pode terminar antes do trabalho encaminhado.

Diagrama de linha do tempo com o laço de eventos iniciando uma espera, um marcador de timeout encerrando a espera e uma thread trabalhadora continuando até um marcador posterior de término.

O timeout corta a ligação de espera no laço; ele não prova que o trabalhador parou.

Observe localmente

Execute como um arquivo .py

O script confirma que o trabalhador começou antes de aplicar o timeout. Os tempos servem apenas para tornar a observação provável; não use a ordem exata dos registros como contrato.

Timeout na espera de asyncio.to_thread

python
import asyncio
import threading
import time


def trabalho_lento(inicio: threading.Event) -> str:
    print("trabalhador: iniciou")
    inicio.set()
    time.sleep(0.30)  # simula uma operação síncrona finita
    print("trabalhador: terminou")
    return "resultado pronto"


async def main() -> None:
    inicio = threading.Event()
    tarefa = asyncio.create_task(asyncio.to_thread(trabalho_lento, inicio))

    # Só iniciaremos o prazo depois de saber que a função começou.
    await asyncio.to_thread(inicio.wait)

    try:
        async with asyncio.timeout(0.05):
            resultado = await tarefa
            print("corrotina: მიიღო", resultado)
    except TimeoutError:
        print("corrotina: timeout; a espera terminou")
        print("tarefa cancelada?", tarefa.cancelled())

    print("corrotina: fim de main")


asyncio.run(main())
print("programa: asyncio.run retornou")

Leia o que o registro demonstra

Atenção

Cancelado não significa interrompido

É esperado observar tarefa cancelada? True e, ainda assim, ver trabalhador: terminou depois. O estado cancelado descreve a tarefa assíncrona que aguardava o resultado; não é um relatório confiável sobre o término da função síncrona já iniciada.

Encerramento também pode esperar

No exemplo, o executor padrão usado por asyncio.to_thread é gerenciado por asyncio.run. Ao encerrar, asyncio.run pode esperar o trabalho pendente desse executor. Por isso, a mensagem programa: asyncio.run retornou tende a aparecer somente após o término do trabalhador.

Com um executor explícito, seu encerramento também pode esperar trabalhadores em andamento. Chamar shutdown(wait=False) evita esperar naquela chamada, mas não interrompe funções que já estão executando nem garante que o processo saia imediatamente.

Verifique a interpretação

Se uma tarefa que aguarda asyncio.to_thread(...) fica cancelada após um timeout, então a função síncrona já iniciada certamente parou.

Explique a sequência

Diagnóstico do registro

Considere este registro possível:

trabalhador: iniciou
corrotina: timeout; a espera terminou
tarefa cancelada? True
corrotina: fim de main
trabalhador: terminou
programa: asyncio.run retornou

Explique por que esse registro não é contraditório e por que shutdown(wait=False) não seria prova de que o trabalhador parou.

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8

Preservar os recursos até o trabalhador terminar

Organize recursos usados por funções síncronas de modo que um timeout assíncrono não os feche enquanto o trabalhador ainda estiver em execução.

Timeout não transfere a posse do recurso

A espera terminou; o trabalho pode não

Depois de um timeout ou cancelamento, a corrotina parou de aguardar o resultado. Isso não prova que a função síncrona parou. Se ela ainda usa um arquivo, conexão ou outro recurso, fechá-lo no finally da corrotina pode interromper ou corromper o trabalho em andamento.

A regra prática é: quem executa o uso do recurso deve controlar sua vida útil até a saída real da função, inclusive quando ocorre uma exceção.

Duas linhas do tempo, responsabilidades diferentes

Diagrama mostrando uma corrotina cuja espera termina por timeout antes de uma thread trabalhadora terminar de usar e fechar um arquivo temporário.

O fim da espera assíncrona acontece antes da limpeza feita pelo próprio trabalhador.

Atenção

Não feche por reflexo

Evite este desenho: a corrotina cria ou recebe um recurso, encaminha uma função que o usa e o fecha no finally logo após o timeout. O finally acompanha a espera da corrotina, não a execução efetiva do trabalhador.

O trabalhador possui o recurso

Abra, use, feche e retorne dados

Prefira passar uma descrição do recurso — por exemplo, um caminho de arquivo — e deixar a função síncrona abrir, usar e fechar o recurso com with. Assim, o fechamento ocorre quando a função realmente sai, seja por sucesso ou exceção.

Isso também é compatível com processos: caminhos, textos e números podem ser serializados; um arquivo já aberto não deve ser enviado ao ProcessPoolExecutor.

Exemplo

Arquivo temporário sob responsabilidade da função síncrona

from pathlib import Path
import time


def ler_arquivo_lentamente(caminho: str) -> str:
    # A função que usa o arquivo também o fecha.
    with Path(caminho).open(encoding="utf-8") as arquivo:
        time.sleep(2)  # Simula uma operação bloqueante finita.
        return arquivo.read()

A corrotina passa apenas str(caminho) para asyncio.to_thread(...) ou para loop.run_in_executor(...). Se a espera assíncrona expirar após a função ter começado, o bloco with continua válido até ler_arquivo_lentamente retornar ou falhar.

Dica

Parada exige cooperação do trabalhador

Um timeout do asyncio não injeta uma interrupção dentro da função síncrona. Quando a operação oferece timeout próprio ou aceita um sinal de parada cooperativa, a própria função deve usá-los e sair; só então sua limpeza acontece. Não confunda abandonar a espera com encerrar o trabalho.

Analise a organização segura

Após um timeout

Um script cria um arquivo temporário e uma função síncrona precisa lê-lo lentamente em uma thread. A corrotina usa um timeout ao aguardar essa função. Qual organização preserva corretamente o recurso?

Checklist de propriedade

Resumo

Decida antes de encaminhar

  • O timeout da corrotina limita a espera, não autoriza fechar recursos que o trabalhador ainda utiliza.
  • Faça a função síncrona abrir, usar e fechar seu recurso; retorne dados, não recursos abertos.
  • Vincule a limpeza à saída real da função, inclusive em exceções.
  • Para processos, passe caminhos e outros dados serializáveis, nunca recursos abertos.
  • Use mecanismos de timeout ou parada oferecidos pela própria operação quando precisar que o trabalhador termine antes.

Passo 8 de 8

Aplicar a integração e verificar o ciclo completo

Reúna as pontes estudadas em um script local: entrada e saída em thread, processamento em processo e timeout que encerra a espera sem necessariamente encerrar o trabalhador.

Mapa da integração completa

Duas pontes, uma coordenação

Em uma aplicação real, prefira uma API assíncrona nativa quando ela for adequada. Quando a dependência necessária for síncrona, mantenha a coordenação no laço e encaminhe o trabalho.

Neste exemplo, asyncio.to_thread recebe uma operação de entrada e saída simulada. Já run_in_executor envia uma função de processamento para um ProcessPoolExecutor. As duas operações são aguardadas com await, sem chamar funções bloqueantes diretamente na thread do laço.

Fronteiras do script

A imagem resume quem executa cada parte e quando o encerramento pode esperar pelo trabalhador.

Diagrama mostrando o laço asyncio no processo principal, uma ponte para uma thread de entrada e saída, outra ponte para um processo de processamento e um timeout que encerra apenas a espera assíncrona.

O timeout delimita a espera no laço; ele não é, por si só, um comando para interromper o processo trabalhador.

Execute o script localmente

Crie um arquivo Python

Salve o código como integracao.py e execute python integracao.py no seu computador. Ele usa apenas a biblioteca padrão e gera os próprios dados.

Os prints do processo usam flush=True para tornar mais visíveis os registros do trabalhador. A ordem exata entre mensagens pode variar.

integracao.py

Script completo com uma operação de entrada e saída simulada, uma operação de processamento e um timeout.

python
import asyncio
import multiprocessing
import time
from concurrent.futures import ProcessPoolExecutor


def ler_dado_local(nome: str, *, atraso: float) -> str:
    """Simula uma chamada síncrona limitada por entrada e saída."""
    print(f"[thread] iniciando leitura de {nome}", flush=True)
    time.sleep(atraso)
    print(f"[thread] leitura concluída", flush=True)
    return f"conteúdo de {nome}"


def somar_quadrados(valores: list[int], atraso: float) -> int:
    """Função de módulo, serializável para o processo trabalhador."""
    print("[processo] processamento iniciado", flush=True)
    time.sleep(atraso)
    total = sum(valor * valor for valor in valores)
    print("[processo] processamento terminado", flush=True)
    return total


async def principal(executor: ProcessPoolExecutor) -> None:
    loop = asyncio.get_running_loop()

    leitura = asyncio.create_task(
        asyncio.to_thread(ler_dado_local, "entrada.txt", atraso=0.2)
    )
    processamento = loop.run_in_executor(
        executor,
        somar_quadrados,
        list(range(10)),
        1.0,
    )

    try:
        resultado_processamento = await asyncio.wait_for(
            processamento,
            timeout=0.3,
        )
        print(f"[laço] soma dos quadrados: {resultado_processamento}")
    except TimeoutError:
        print("[laço] timeout: a espera terminou; o processo pode continuar")

    resultado_leitura = await leitura
    print(f"[laço] resultado da leitura: {resultado_leitura}")
    print("[laço] corrotina principal terminou")


if __name__ == "__main__":
    contexto = multiprocessing.get_context("spawn")
    with ProcessPoolExecutor(mp_context=contexto) as executor:
        asyncio.run(principal(executor))
    print("[síncrono] executor encerrado; programa finalizado")

Leia os sinais sem supor uma ordem rígida

O que verificar

Procure por estes fatos, não por tempos ou uma intercalação exata:

  • A leitura na thread termina e seu resultado chega ao laço por await.
  • O laço registra o timeout da espera pelo processamento.
  • O processo pode registrar seu término depois do timeout.
  • A mensagem final síncrona só aparece após a saída do with ProcessPoolExecutor, que pode esperar o trabalhador terminar.

Assim, o fim da espera não transfere para a corrotina a propriedade do trabalho nem autoriza encerrar recursos que o trabalhador ainda usa.

Relate a execução

Após executar o arquivo, relate: por que cada ponte foi escolhida, onde o executor é encerrado e qual registro mostra que o trabalhador pode continuar depois do timeout.

Escreva pelo menos 180 caracteres (0/180).

Síntese e aplicação final

Resumo

Checklist para integrar chamadas bloqueantes

Use esta sequência ao integrar uma dependência síncrona a uma corrotina.

  • Prefira uma API assíncrona adequada antes de criar uma ponte para código síncrono.
  • Passe a função e seus argumentos para to_thread ou run_in_executor; não a execute antecipadamente no laço.
  • Use await para obter retorno ou exceção sem bloquear a thread do laço.
  • Para um executor explícito, crie-o e encerre-o no escopo síncrono que envolve asyncio.run.
  • Um timeout ou cancelamento da espera não prova que uma função já iniciada em thread ou processo parou.
  • Mantenha recursos sob responsabilidade do trabalhador até a saída real dele.

Decisão de integração

Você precisa integrar uma biblioteca síncrona de entrada e saída e uma etapa de processamento Python demorada. Explique, de forma concisa, qual ponte usaria em cada caso e o que um timeout não garante.

Escreva pelo menos 120 caracteres (0/120).

Tutorial concluído

Parabéns! Você concluiu: Integrar chamadas bloqueantes ao asyncio

Muito bem! Você sabe encaminhar chamadas bloqueantes sem executá-las no laço, aguardar resultados de forma assíncrona e distinguir timeout da espera de término efetivo do trabalho.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso