Introdução à programação assíncrona em Python

1. O que é assíncrono?

Falando em modelo assíncrono, temos que citar o modelo síncrono comum, que são conceitos relativos.

O modelo de sincronização significa que o programa deve ser executado sequencialmente.Quando o programa executa uma operação que precisa esperar por recursos externos (envio e recebimento de dados de rede, leitura e gravação de arquivos), ele entrará em um estado e só continuará a executar depois que os recursos externos estiverem disponíveis 阻塞. Em contrapartida, o modelo assíncrono tem 非阻塞a característica de que o programa continuará executando outro código enquanto espera por recursos externos.

Na versão 3.4, o Python introduziu suporte para programação assíncrona. No mesmo thread, várias corrotinas são agendadas através do loop de eventos para alternar fatias de código, o que pode eliminar o impacto negativo do bloqueio no desempenho do programa e sobrecarga adicional de thread causada pela comutação .

Em um thread, os usuários podem criar tarefas assíncronas (como corrotinas) e entregá-las ao loop de eventos ( event loop) para agendamento e gerenciamento unificados. Uma corrotina ( Coroutines) é uma unidade de execução de código um nível menor que um thread. Ao mesmo tempo, apenas uma corrotina no loop de eventos está em execução.Quando a corrotina aguarda recursos externos, o thread não será bloqueado , mas o direito de execução será entregue , para que o loop de eventos continue a executar outras corrotinas . O texto acima é o conteúdo básico do modelo assíncrono Python, e a operação específica será descrita em detalhes no Capítulo 3 posteriormente.

Em segundo lugar, por que assíncrono?

Antes de aprender como programar de forma assíncrona em Python, pense duas vezes: por que escolher assíncrono em seu código?

Primeiro, a vantagem de desempenho . Para o interpretador Python que usamos todos os dias, GIL parece ser um tópico inevitável (GIL é detalhado no próximo blog Python multithreading ). Devido à limitação do GIL, a CPU possui, na verdade, apenas um thread em execução ao mesmo tempo e o desempenho simultâneo é bastante limitado. Quando um thread entra em estado bloqueado, o bloqueio GIL é liberado e o direito de execução é transferido para outros threads para continuar a execução. À primeira vista, parece muito semelhante ao princípio de execução de corrotinas que mencionamos acima.

Na verdade, o processo é muito semelhante: quando a corrotina encontra operações IO durante a execução, ela muda para outras corrotinas para continuar a execução. No entanto, em um cenário multithread, a sobrecarga causada pelo bloqueio e troca de threads é muito maior do que o custo do agendamento de corrotinas, e os recursos de memória ocupados pelas corrotinas são muito menores que os threads . Portanto, usando corrotinas, a transferência de fatias de código pode ser concluída em um thread a um custo muito pequeno. Esta lacuna, quando colocada em cenários de aplicação específicos, trará melhorias consideráveis ​​de desempenho.

Além disso, experiência em programação . Os desenvolvedores não precisam mais considerar uma série de questões, como competição e liberação de recursos de bloqueio, problemas de impasse, sincronização de threads, etc., portanto, a programação assíncrona é um modelo de programação relativamente mais simples e intuitivo.

3. Como ser assíncrono?

Esta seção asynciose concentra nas operações relacionadas da biblioteca IO assíncrona na biblioteca padrão Python. Existem outras bibliotecas que também suportam programação assíncrona, como Twisted, Tornado, etc. Amigos interessados ​​podem aprender sobre isso depois.

3.1 Corrotinas

As corrotinas fornecem o suporte mais básico para o modelo de programação assíncrona do Python. Considere o seguinte programa de exemplo:

import asyncio

async def func():
	print('Hello World, Asynchronous.')
	return 114514

async def main():
	res = await func()
	print(f'main func executed.{res}')
	
>>> func() # 协程函数的调用结果是一个协程对象
<coroutine object func at 0x000001D9C01C59C0>

>>> await func() # 通过await关键字可以执行协程对象,并获取真正的返回值。
Hello World, Asynchronous.
114514

>>> asyncio.run(main()) # 隐式创建新的事件循环,并执行协程对象。
Hello World, Asynchronous.
main func executed.114514

Declaramos asyncdois via palavras-chave 协程函数. O resultado de uma chamada para uma função de co-rotina é one 协程对象. awaitO objeto co-rotina pode ser executado por meio de palavras-chave e o valor real de retorno pode ser obtido. Deve-se observar que se outra função de corrotina precisar ser chamada de forma assíncrona dentro de uma função, a própria função também deverá ser uma função de corrotina . Aqui mainestá uma função de rotina, para que ela possa chamar funca função.

Mencionamos anteriormente que os usuários podem entregar corrotinas ao loop de eventos para gerenciar o agendamento. awaitO princípio é assim. Se houver um loop de eventos no estado de execução no thread atual, o objeto de corrotina será entregue a ele para agendamento. Caso contrário, um novo loop de eventos será criado e habilitado.

Da mesma forma, asyncio.run()a corrotina também pode ser executada através do loop de eventos, a diferença é que asyncio.run()ela se destina a ser usada como ponto de entrada da função, o que forçará a criação de um novo loop de eventos , portanto este método é proibido quando houver outros loops de eventos ativos no thread atual. Caso contrário, será lançado RuntimeError.

3.2 Aguardáveis

Objetos que podem ser awaitrecebidos e processados ​​por palavras-chave são chamados de objetos aguardáveis, e tais objetos também podem ser recebidos e executados pelo loop de eventos. Embora possamos __await__personalizar o objeto aguardável implementando métodos, geralmente não é recomendado fazer isso. Na maioria dos casos, usamos os três tipos de objetos aguardáveis ​​fornecidos pelo Python, que são Coroutine, Taske Future.

Encapsular Coroutineobjetos como Taskobjetos pode obter recursos de despacho mais ricos. Veja a seguir um programa de exemplo:

async def func():
	print('func executed.')

# 通过asyncio将func()协程对象封装为任务对象
task1 = asyncio.create_task(func(), name='task 01')

# 等价于通过事件循环
loop = asyncio.get_event_loop()
task2 = loop.create_task(func(), name='task 02')

Ao contrário Coroutinesdo objeto que precisa ser executado através do loop de eventos após a criação, Tasko próprio objeto é criado através do loop de eventos, o que significa que quando você está create_taskem execução, a corrotina interna já está em execução.

FutureÉ um objeto aguardável que fornece uma API de nível inferior, que representa o resultado de uma operação assíncrona, e também pode ser usado para vincular uma corrotina que ainda não concluiu a execução. As propriedades e métodos Taskherdados fornecem uma API assíncrona de nível superior junto com ele. De modo geral, não é recomendado criar e fazer chamadas assíncronas diretamente, e o conteúdo sobre isso será discutido em um artigo posterior.FutureCoroutineFutureFuture

3.3 Criação e cancelamento de tarefas

TaskÉ uma ferramenta importante na programação assíncrona Python. Podemos Taskaprender o estado atual da corrotina e realizar uma determinada faixa de agendamento.

3.3.1 Criar

O método de criação de uma tarefa, conforme mostrado no código 3.2, pode ser através asyncioda biblioteca ou diretamente através do loop de eventos, que são essencialmente os mesmos. Nota: create_taskQuando estiver em , certifique-se de salvar Taskuma referência a esse valor de retorno. Como o loop de eventos só mantém uma referência fraca ao Taskobjeto após recebê-lo, se o valor de retorno não for mantido adequadamente, a tarefa pode ser coletada como lixo a qualquer momento , independentemente de a corrotina correspondente à tarefa ser executada ou não.

3.3.2 Cancelamento

A corrotina encapsulada pela tarefa é agendada para execução pelo loop de eventos. O loop de eventos executa apenas uma corrotina por vez.Quando uma corrotina entrega o direito de execução, o loop de eventos irá agendar e determinar a próxima corrotina a ser executada. Antes de terminar a execução de uma corrotina, podemos Task.cancel()cancelar sua execução através do método, conforme mostrado no código a seguir.

import asyncio

# 定义一个协程函数
async def func():
	count = 0
	while True:
		await asyncio.sleep(1)
		print(f'awake {count := count + 1} time(s).')

# 创建任务
task1 = asyncio.create_task(func(), name="task1")
task2 = asyncio.create_task(func(), name="task2")

task1.cancel() # 取消task1的执行

cancel()O princípio é adicionar +1 à tarefa alvo.No 取消请求计数próximo ciclo do loop de eventos, se o número de solicitações de cancelamento da corrotina for maior que 0, uma será passada para a função da corrotina CancelledErrorpara encerrar sua execução continuada.

Portanto, antes de jogá-lo oficialmente na corrotina CancelledError, temos a oportunidade de utilizar uncancel()o método para retirar a solicitação de cancelamento da tarefa, que contará a solicitação de cancelamento -1. No exemplo acima, podemos:

# 交出执行权
task1.cancel()    # cancel_count += 1, curr = 1
task1.cancel()    # cancel_count += 1, curr = 2
task1.uncancel()  # cancel_count -= 1, curr = 1
task1.uncancel()  # cancel_count -= 1, curr = 0
# 下次事件循环,由于取消请求计数为0,不会取消task1的执行

Claro, mesmo que CancelledErrora função de corrotina seja realmente passada, também podemos try... except...capturar a exceção por meio da instrução, para evitar a interrupção da função de corrotina. No entanto, a tarefa ainda será marcada como cancelada e ainda precisaremos chamar a uncancel()solicitação de desfazer cancelamento se quisermos que o loop de eventos continue.

Alternativamente, podemos envolvê-lo com asyncio.shield()will Task, o que também evita que a tarefa seja cancelada.

task = asyncio.create_task(func())
await asyncio.shield(task)

# 由于如果直接await协程的话,协程是无法被取消的,因此上面的操作等价于
await func()

Além disso, para tarefas que são normalmente executadas, podemos Task.done()julgar se a tarefa terminou Task.result()obtendo o resultado da execução da tarefa, se o resultado não estiver disponível, será lançado InvalidStateError, e se a tarefa que obteve o resultado foi cancelada, será será lançado CancelledError.

3.4 Suspensão, tempo limite, espera

3.4.1 Sono

asyncio.sleep()é uma função que usamos com frequência. Ele fará com que a corrotina durma pelo número de segundos que especificamos, semelhante ao time.sleep()método no modelo de programação síncrona, a diferença é que asyncio.sleep()não bloqueará a thread, mas fará com que a corrotina atual entregue a execução corretamente.

Este recurso é muito útil. Conforme mencionado acima, o loop de eventos só pode executar uma corrotina por vez. Durante a execução normal de uma corrotina, a menos que encontre yieldou awaitoperações IO, ela não entregará o direito de execução. Isso fará com que outras tarefas de rotina não sejam executadas. E através asyncio.sleep()do método, mesmo que o número de segundos seja definido como 0, a corrotina pode entregar imediatamente o direito de execução e aguardar o próximo agendamento do loop de eventos.

3.4.2 Tempo limite

Para evitar que tarefas assíncronas, como solicitações de rede assíncronas e operações IO assíncronas, demorem muito, de modo que a corrotina não possa ser executada normalmente, podemos usar o mecanismo de tempo limite para planejar a execução da corrotina dentro de um limite de tempo.

asyncio.timeoutÉ um gerenciador de contexto assíncrono que pode definir um tempo limite. Neste contexto assíncrono, quando a execução da corrotina expirar, ela será lançada TimeError. Os dados de tempo aqui não são um intervalo de tempo, mas o tempo decorrido desde o início do loop de eventos atual. Se não soubermos a hora em que ativamos o contexto de tempo limite, podemos defini-lo temporariamente como None(sem tempo limite). Depois de entrar no contexto, use time()o método do objeto de loop de eventos para obter o tempo de execução decorrido e, em seguida, reschedule()planeje novamente o tempo limite por meio do método.

async def main():
    try:
        # 启动异步超时上下文管理器
        async with asyncio.timeout(None) as cm:
            # 获取当前事件循环已运行时间,并计算超时时刻
            new_deadline = get_running_loop().time() + 10
            # 重新规划超时
            cm.reschedule(new_deadline)
			# 执行协程
            await long_running_task()
    except TimeoutError:
    	# 捕获超时异常,同时协程得以继续运行
        pass
	# 通过上下文管理器的expired方法判断是否发生过超时
    if cm.expired():
        print("发生超时.")

O loop de eventos inicializa um relógio monotônico na inicialização e o atualiza a cada iteração do loop. Em cada iteração do loop, o loop de eventos verifica a diferença entre o horário atual e a iteração do loop anterior e a adiciona ao relógio monotônico para obter o horário mais recente. Na configuração de tempo limite acima, se o valor do tempo for menor que loop.time(), o tempo limite será acionado imediatamente quando o loop de eventos for iterado.

Quando ocorre um tempo limite, todas as tarefas inacabadas no contexto serão canceladas e as lançadas CancelledErrorserão convertidas em TimeoutErrorlançamentos unificados.

3.4.3 Esperando

Além do mecanismo bruto de tempo limite, também podemos asyncio.wait()aguardar um lote de tarefas e, após o tempo limite, dois conjuntos de tarefas concluídas e inacabadas serão retornados, o que é conveniente para processarmos separadamente.

import asyncio
async def func(delay: int):
	await asyncio.sleep(delay)
# 超时时间设置为5,对于执行时间1~10的10个协程来说,会有一半完成,另一半未完成,这两个集合都会返回
done, pending = await asyncio.wait([asyncio.create_task(func(i)) for i in range(1, 11)], timeout=5)
print(len(done), len(pending)) # 5 5

Claro, além de definir as regras de tempo limite de espera, você também pode return_whendefinir outras regras por meio de parâmetros. Principalmente, as três constantes a seguir podem ser passadas:

valor do parâmetro descrever
FIRST_COMPLETED retornar quando alguém concluir
FIRST_EXCEPTION Se for lançada uma exceção na tarefa executada, ela retornará imediatamente, caso contrário será equivalente a ALL_COMPLETED
ALL_COMPLETED Retorna quando todas as tarefas são executadas ou canceladas

No exemplo acima, o resultado obtido pela aplicação das três regras acima é:

done, pending = await asyncio.wait([asyncio.create_task(func(i)) for i in range(1, 11)], return_when=asyncio.FIRST_COMPLETED)
# FIRST_COMPLETED 1已完成 9未完成
# FIRST_EXCEPTION 10已完成 0未完成
# ALL_COMPLETED   10已完成 0未完成

3.5 Atribuição de Thread

Embora os métodos síncronos em Python atualmente tenham versões assíncronas disponíveis para uso dos desenvolvedores. Mas e se você encontrar um método síncrono que deve ser chamado em sua função de corrotina? Uma vez chamado, o thread onde a corrotina está localizada é bloqueado diretamente, e o loop de eventos, a eficiência da corrotina, etc. de que falamos antes estão fora de questão.

Portanto, para evitar que o thread importante onde a corrotina está localizada seja bloqueado, podemos fazer um acordo, asyncio.to_thread()agrupar o método de sincronização em uma corrotina por meio do método e criar outro thread para executá-lo.

import time
import asyncio
# 定义同步方法
def blocking_io():
    print(f"{time.strftime('%X')} 阻塞开始")
    # 使用time.sleep()来指代任意的同步方法,例如IO、网络请求、文件读写操作等
    time.sleep(1)
    print(f"{time.strftime('%X')} 阻塞结束")
    
async def main():
    print(f"协程从 {time.strftime('%X')} 开始执行")
    await asyncio.gather(
    	# 使用asyncio.to_thread封装一个同步函数,该方法返回一个协程
        asyncio.to_thread(blocking_io),
        asyncio.sleep(1))
        
    print(f"所有协程到 {time.strftime('%X')} 执行结束")
    
asyncio.run(main())
# 执行结果
#>>> 协程从 22:02:22 开始执行
#>>> 22:02:22 阻塞开始
#>>> 22:02:23 阻塞结束
#>>> 所有协程到 22:02:23 执行结束

Durante a atribuição do thread, todos os parâmetros passados ​​para a função de corrotina serão passados ​​para outro thread. Além disso, as variáveis ​​de contexto entre threads não são compartilhadas originalmente, mas os threads criados por meio da atribuição de threads recebem ativamente as variáveis ​​de contexto do thread original. Isso garante que a função de co-rotina possa ser executada corretamente em outro thread.

Verifica-se que o thread onde a corrotina está localizada não está bloqueado, e o programa leva 1s no total e é executado conforme programado. Deve-se notar que, devido à influência do GIL, to_thread()ele só pode melhorar o desempenho de métodos de sincronização com uso intensivo de IO , enquanto para métodos de sincronização com uso intensivo de CPU, a CPU só pode executar um thread por vez, portanto, mesmo que o bloco seja transferido para outros tópicos, também não tem efeito óbvio.

Além de criar temporariamente um thread, se nosso próprio ambiente de programação for multithread, podemos asyncio.run_coroutine_threadsafeatribuir a corrotina ao loop de eventos de um thread especificado para executar através do método, de modo a realizar o agendamento flexível da corrotina entre vários threads .

# loop1是来自另一个线程的正在运行的事件循环
future = asyncio.run_coroutine_threadsafe((coro:=asyncio.sleep(10)), loop)
# 返回值是一个Future对象,我们可以用与处理Task类似的方法处理它
try:
	# 获取结果,如果没执行完就等待一段时间
    result = future.result(timeout)
except TimeoutError:
	# 超时会触发超时异常,这个时候我们可以手动取消它
    print('协程执行超时,正在手动取消...')
    future.cancel()
except Exception as exc:
    print(f'协程函数抛出的其他异常: {exc!r}')
else:
	# 如果没问题的话,可以打印结果
    print(f'协程执行结果是{result!r}.')

O texto acima é todo o conteúdo deste artigo, e meu nível é limitado. Se houver alguma inadequação, sinta-se à vontade para me esclarecer.

4. Referências

[1] Documentação oficial do Python: corrotinas e tarefas
[2] Documentação oficial do Python: Loop de eventos

Acho que você gosta

Origin blog.csdn.net/qq_38236620/article/details/131151970
Recomendado
Clasificación