异步编程asyncio
py异步编程当中的协程、事件循环、任务的概念:
协程
协程(coroutine):由async def定义且可以暂停和恢复运行的函数,称为协程函数。协程函数在执行时并不会真正执行里面的代码,而是会返回一个协程对象(coroutine object)。当运行协程对象时才会真正执行里面的代码。
py
import asyncio
async def coroutine_func():
print("Coroutine started")
await asyncio.sleep(1)
print("Coroutine finished")
print(coroutine_func()) # <coroutine object coroutine_func at 0x000001B246121380>
await关键字作用:
- await关键字可以暂停当前代码运行并让出控制权,当await后面的代码执行完毕时再去请求控制权并恢复运行,具有控制权的函数可以继续执行,而没有控制权的函数只能等待执行。
- await也可以将后面的协程包装成任务被事件循环调度。
- await会获取任务执行的结果并返回
事件循环
事件循环用于调度任务执行。事件循环的三种状态,分别是:检查协程、让出控制、等待协程
任务
任务是对协程的封装,包含协程的代码以及协程的状态(如运行或者等待状态等),当一个协程被包装成任务,就会被事件循环调度执行。
await可以将协程包装成任务
py
async def read_file(filepath):
print(f"Reading file...")
await asyncio.sleep(2) # 这一段异步代码是一个任务
print(f"File read")
return filepath
当我们需要创建异步编程函数时,只需要实现三个步骤:
- 定义协程函数
- 包装协程为任务
- 创建事件循环、将任务添加到事件循环并运行
首先定义协程函数,只需要将异步函数通过关键字async def去定义,然后将异步操作使用await去封装,await会让出当前控制权并等待当前协程执行完后再申请控制权(await后面只有接协程才会执行后面的代码,我们可以使用关键字asyncio
将当前操作封装为协程)
包装协程为任务有几种方法
- 代码段可以通过await关键字实现,可以将当前代码包装成任务
- 通过
create_task
方法封装协程并立即返回一个task到事件循环内调度,后续可以使用await等待任务执行并获取结果,或者使用asyncio.gather()
等方法聚合多个task执行
创建事件循环有两种方式,手动和自动创建事件循环
-
手动创建事件循环,及将创建任务和获取结果分开进行,用于添加一些处理事件结果的操作
pyasync def main(): url = 'example.com' filepath = 'example.txt' # 手动分步创建任务并添加到事件循环,获取任务执行结果 task1 = asyncio.create_task(fetch_url(url)) # create_task() 将协程封装成一个任务对象,并将其添加到事件循环中 task2 = asyncio.create_task(read_file(filepath)) fetch_result = await task1 # 使用await关键字获取事件循环任务结果 read_result = await task2 print(fetch_result) # example.com print(read_result) # example.txt if __name__ == '__main__': start_time = perf_counter() asyncio.run(main()) # 执行异步事件循环入口,将协程调度到事件循环内运行,当执行结束关闭异步循环 end_time = perf_counter() print(f'Time taken:{end_time - start_time} seconds') # 总共运行2s左右
-
当我们不需要处理事件循环获取结果的逻辑时,一般采用方法直接获取事件循环结果即可.
asyncio.gather()
等待所有任务执行完后,返回结果列表asyncio.as_completed()
当一有任务执行完,优先返回,返回的结果是一个迭代器- 以上两个函数可以传入协程或者任务
pyasync def main(): url = 'example.com' filepath = 'example.txt' # 自动创建任务并添加到事件循环,获取任务执行结果 results = asyncio.gather(fetch_url(url), read_file(filepath)) # 等待所有任务执行完后,返回结果列表 print(await results) # ['example.com', 'example.txt'] results2 = asyncio.as_completed([fetch_url(url), read_file(filepath)]) # 当一有任务执行完,先返回结果,返回的结果是一个迭代器 for result in results2: print(await result) # example.txt example.com if __name__ == '__main__': start_time = perf_counter() asyncio.run(main()) # 执行异步事件循环入口,将协程调度到事件循环内运行,当执行结束关闭异步循环 end_time = perf_counter() print(f'Time taken:{end_time - start_time} seconds') # 总共运行2s左右
-
我们也可以直接获取事件循环对象,对事件循环做一些操作,即使用
loop = asyncio.get_event_loop()
获取事件循环对象pyloop = asyncio.get_event_loop() # 我们拿到事件循环对象后可以对事件循环做一些自定义的配置操作等
同步函数添加事件循环(也称阻塞函数)
已知当我们需要异步处理函数时,需要将同步函数定义为协程,但如果我们不想重构函数时,可以使用to_thread方法使用多线程处理同步函数。
to_thread(func, args)
将同步函数使用多线程执行,传入两个参数,第一个是同步函数,后面接函数的参数
py
# 将同步函数使用多线程转为异步处理
results2 = asyncio.as_completed([fetch_url(url), read_file(filepath), asyncio.to_thread(foo, 1, 2, 3)]) # to_thread将同步函数使用多线程执行,传入两个参数,第一个是同步函数,后面接函数的参数
for result in results2:
print(await result) # (1, 2, 3), example.com, example.txt # 因为1,2,3执行比较快所以先返回
if __name__ == '__main__':
start_time = perf_counter()
asyncio.run(main()) # 执行异步事件循环入口,将协程调度到事件循环内运行,当执行结束关闭异步循环
end_time = perf_counter()
print(f'Time taken:{end_time - start_time} seconds') # 总共运行2s左右
或者将同步函数添加到事件循环内异步执行,使用loop.run_in_executor(executor, lambda func)
实现
executor执行器
为None表示使用默认的线程池lambda func
表示需要执行的函数调用
py
loop = asyncio.get_event_loop()
await loop.run_in_executor(executor, lambda 阻塞函数) # # 在异步线程池中执行同步命令