本章将介绍在asyncio中使用单线程并发模型编写程序的基础知识
1 关于协程
1.1 使用async关键字创建协程
async def my_coroutine() -> None:
print('hello asyncio')
async def coroutine_add_one(number:int) -> int:
return number + 1
def add_one(number:int) -> int :
return number + 1
function_result = add_one(1)
coroutine_result = coroutine_add_one(1)
print(f'function_result:{function_result},type:{type(function_result)}')
print(f'coroutine_result:{coroutine_result},type:{type(coroutine_result)}')
print(f'my_coroutine:{my_coroutine},type:{type(my_coroutine)}')
输出结果:
可以发现,直接调用def
函数可以返回一个预期中的整数,但是直接调用coroutine_add_one
,并不会执行协程中的代码,而是返回一个协程对象。
1.2 运行协程
在python3.7的版本中,如果不存在事件循环,则必须创建一个事件循环。但在asyncio
库中添加了几个抽象事件循环管理的函数,可以使用asyncio.run
去运行协程,代码如下:
import asyncio
async def coroutine_add_one(number:int) -> int:
return number + 1
result = asyncio.run(coroutine_add_one(1))
print(result)
输出结果
asyncio.run
首先创建一个全新的事件,然后接受传递给它的任何协程,并运行它直到完成,然后返回结果,同时还会对主协程完成后可能继续执行的任何内容进行清理,最后会关闭并结束事件循环
1.3 使用await
关键字暂停执行
asyncio
的真正优势是能够暂停执行,即让事件循环在长时间运行的操作期间,运行其他任务。为了达到这个目的,可以使用await
关键字,使用该关键字会导致后面的协程运行,但并不会产生一个协程对象。
await
会暂停它包含的协程,直到等待的协程完成并返回结果。
import asyncio
async def coroutine_add_one(number:int) -> int:
return number + 1
async def main() -> None:
one_plus_one = await coroutine_add_one(1) # 暂停,直到coroutine_add_one(1)返回
two_plus_one = await coroutine_add_one(2) # 暂停,直到coroutine_add_one(2)返回
print(one_plus_one, two_plus_one)
asyncio.run(main())
输出结果
在该代码中,执行了两次暂停,首先等待对coroutine_add_one(1)
的调用,一旦返回结果,主函数将停止暂停,运行coroutine_add_one(2)
。执行流程可视化如下图所示:
1.4 使用sleep引入长时间运行的协程
上一个例子的代码和普通代码的运行没有区别,该节的示例将展示如何通过在等待时引入虚拟休眠操作来运行其他代码。
使用asyncio.sleep
让协程“休眠”给定的秒数,会在预定的时间内暂停协程,以模拟对数据库或者Web API进行长时间调用的情况,注意,asyncio.sleep
本身是一个协程,所以需要配合await
使用
import asyncio
async def hello_world():
await asyncio.sleep(1) # 暂停hello_world()一秒
return "Hello World"
async def main():
message = await hello_world() # 暂停main()函数,直到hello_world()函数返回结果
print(message)
asyncio.run(main())
输出结果
在接下来的示例中,我们将大量使用sleep
,所以我们可以创建一个可以重复使用的协程,它会休眠并输出一些东西,称之为协程delay
1.5 运行两个协程
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def add_one(number:int):
return number + 1
async def hello_world():
await delay(1)
return "hello world"
async def main():
message = await hello_world()
one_plus_one = await add_one(1)
print(one_plus_one)
print(message)
asyncio.run(main())
输出结果
该代码的执行流程可视化如下图:
该代码还是一个串行运行,并没有实现在运行hello_world()
时,立即输出add_one(1)
的结果。如果想摆脱这种顺序模型,需要引入“任务”概念。
2 通过任务实现并行
任务是协程的包装器,它安排协程尽快在事件循环上运行,这种调度以非阻塞方式发生,即意味着可以大致同时执行多个任务
2.1 创建任务
通过asyncio.create_task()
创建任务,当调用该函数时,给它一个协程,它会立即返回一个任务对象,就可以使用await
运行该任务
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def main():
sleep_for_three = asyncio.create_task(delay(3))
print(type(sleep_for_three))
result = await sleep_for_three
print(result)
asyncio.run(main())
输出结果
2.2 同时运行多个任务
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def main():
sleep_for_three = asyncio.create_task(delay(3))
sleep_again = asyncio.create_task(delay(3))
sleeep_once_more = asyncio.create_task(delay(3))
await sleep_for_three
await sleep_again
await sleeep_once_more
asyncio.run(main())
输出结果
代码中,休眠了3次每次3秒,理论上需要9秒才能输出完成,但实际上是几乎同时输出的,只用了大约3秒!执行流程可视化如下图所示:
当任务一调用delay(3)
后,开始执行await asyncio.sleep(3)
,此时delay(3)
暂停,等待asyncio.sleep(3)
返回结果,所以此时开始执行任务二,同理,在任务二等待asyncio.sleep(3)
返回结果时,开始执行任务三。
该方法,并发的执行了三个任务,如果这三个任务是对数据库或者Web API进行长时间调用,则可以有效的利用这些时间。
如果我们添加到十个任务,仍然只需要大约3秒,但如果是串行运行则需要30秒。
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def main():
sleep_for_three = asyncio.create_task(delay(3))
sleep_again = asyncio.create_task(delay(3))
sleeep_once_more = asyncio.create_task(delay(3))
sleeep_once_more_1 = asyncio.create_task(delay(3))
sleeep_once_more_2 = asyncio.create_task(delay(3))
sleeep_once_more_3 = asyncio.create_task(delay(3))
sleeep_once_more_4 = asyncio.create_task(delay(3))
sleeep_once_more_5 = asyncio.create_task(delay(3))
sleeep_once_more_6 = asyncio.create_task(delay(3))
sleeep_once_more_7 = asyncio.create_task(delay(3))
await sleep_for_three
await sleep_again
await sleeep_once_more
await sleeep_once_more_1
await sleeep_once_more_2
await sleeep_once_more_3
await sleeep_once_more_4
await sleeep_once_more_5
await sleeep_once_more_6
await sleeep_once_more_7
asyncio.run(main())
2.3 当其他操作完成时运行代码
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def hello_word():
for i in range(2):
await asyncio.sleep(1)
print(f"hello world {i}")
async def main():
sleep_for_three = asyncio.create_task(delay(3))
sleep_again = asyncio.create_task(delay(3))
await hello_word()
await sleep_for_three
await sleep_again
asyncio.run(main())
运行结果
该代码创建了两个任务,新建了一个协程,内容是每秒输出一个hello world
,把该代码放在第一个执行的地方,但是却出现在中间的位置。可视化执行流程如下:
首先启动两个任务,每个任务休眠3秒,当两个任务空闲时,开始执行hello_word
。
但是该模式有一个潜在的问题,即任务完成的时间不确定,如果一个任务需要很长的时间才能完成,我们可能需要让其停止,这里便可以对任务使用取消操作。
3 取消任务和设置超时
有时网络连接会出问题,导致现有的请求无法处理,对此我们不能无限等待,所以需要指定超时来处理这种情况
3.1 取消任务
每个协程对象都有一个cancel()
方法,调用就可以取消该任务,同时引发一个CancelledError,可以捕捉该错误进行一些操作。
3.2 设置超时并使用wait_for
执行取消
asyncio
通过名为asyncio.wait_for
函数,实现以秒为单位指定超时时间,接受一个协程或任务,然后返回一个可以等待的协程。如果任务完成所需时间超过了设定的时间,则引发TimeoutException,任务会自动取消
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def main():
delay_task = asyncio.create_task(delay(2))
try:
result = await asyncio.wait_for(delay_task, timeout=1) # 如果1秒没有返回结果,则抛出TimeoutError异常
print(result)
except asyncio.TimeoutError:
print("timeout")
asyncio.run(main())
输出结果
3.3 保护任务免于取消
某些情况下,我们希望协程能够保持运行,比如某些任务花费的时间比预期的长,但是在超过设定时间时,也不取消任务,可以使用asyncio.shield
函数包装任务,该函数可以防止传入的协程被取消。
import asyncio
async def delay(delay_seconds:int): # 接收一个整数参数delay_seconds,表示希望异步等待的秒数
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
async def main():
delay_task = asyncio.create_task(delay(4))
try:
result = await asyncio.wait_for(asyncio.shield(delay_task), timeout=1) # 如果1秒没有返回结果,则抛出TimeoutError异常
print(result)
except asyncio.TimeoutError:
print("timeout")
result = await delay_task
print(result)
asyncio.run(main())
输出结果
可以发现,虽然报了timeout
的错误,但是delay_task
在报错后依然在运行,4秒后输出了预期的结果,就是因为asyncio.shield
阻止任务被取消。
4 任务、协程、future 和 awaitable
为了理解为什么任务和协程都能在await
中使用,就需要理解future
和awaitable
,这是理解asyncio内部工作原理的关键。
4.1 关于 future
future是一个Python对象,包含了一个希望在未来某个时间点获得但目前可能还不存在的值。
当创建future时,它没有任何值,一旦得到了一个结果,就可以设置future,这时我们才能从future中提取结果。
import asyncio
my_future = asyncio.Future()
print(f'my_future:{my_future.done()}')
my_future.set_result(1234)
print(f'my_future:{my_future.done()}')
print(f'my_future:{my_future.result()}')
输出结果
在设置future前是False,设置后是True
4.1.1 等待一个future
future也可以在await
中使用,对其执行该操作相当于“暂停,直到future被设置了一个值,再唤醒并处理”
import asyncio
def make_request():
future = asyncio.Future()
asyncio.create_task(set_future_value(future))
return future
async def set_future_value(future):
await asyncio.sleep(1) # 模拟一个异步操作,1秒后才给future设置值
future.set_result(42)
async def main():
future =make_request()
print(f'future:{future.done()}')
value = await future # 等待future完成
print(f'future:{future.done()}')
print(f'value:{value}')
asyncio.run(main())
输出结果
4.2 future、任务、协程之间的关系
如下图所示:
任务是直接继承自future,future、任务、协程都是继承自awaitable抽象基类
4.3 使用装饰器测量协程执行时间
将下列函数使用装饰器放置在任何协程上,都可以看到运行了多长时间
import asyncio
import functools
from typing import Callable
import time
def async_timed():
def wrapper(func: Callable):
@functools.wraps(func)
async def wrapped(*args, **kwargs):
print(f"starting {func} with args {args} {kwargs}")
start = time.time()
try:
return await func(*args, **kwargs)
finally:
end = time.time()
total = end - start
print(f"finished {func} in {total} seconds")
return wrapped
return wrapper
4.4 使用装饰器对两个并发任务进行计时
from util.auxiliary_function import delay,async_timed
import asyncio
@async_timed()
async def delay(delay_seconds:int):
print(f"delay {delay_seconds} seconds")
await asyncio.sleep(delay_seconds)
print(f'finshed sleep for {delay_seconds} seconds')
return delay_seconds
@async_timed()
async def main():
task_one = asyncio.create_task(delay(2))
task_two = asyncio.create_task(delay(3))
await task_one
await task_two
asyncio.run(main())
输出结果
两个delay
分别在2和3秒内开始和结束,主协程也只花了3秒
5 协程和任务的陷阱
可以看到使用协程和任务可以加速程序的性能,但不是所有的应用程序都适合使用异步编程。
在把程序异步化时会出现两个主要错误:
- 尝试在不使用多处理的情况下,在任务或者协程中运行CPU密集型代码
- 使用阻塞I/O密集型API而不使用多线程
5.1 运行CPU密集型代码
可能有执行大量计算的函数,如果有几个可能同时运行的函数,可能会试图使用异步编程,但是asyncio是使用单线程并发模型,依然受到单线程和全局解释锁的限制,下面是一个尝试同时运行多个CPU密集型函数的代码
from util.auxiliary_function import delay,async_timed
import asyncio
@async_timed()
async def cpu_bound_work():
counter = 0
for i in range(100000000):
counter += 1
return counter
@async_timed()
async def main():
task_one = asyncio.create_task(cpu_bound_work())
task_two = asyncio.create_task(cpu_bound_work())
await task_one
await task_two
asyncio.run(main())
输出结果
可以看到,依然是先完成一个函数的计算,再做第二个函数的计算,运行的时间是二者调用时间的总和,并没有加速。
我们在后面再加一个delay任务(非CPU密集型任务)
from util.auxiliary_function import delay,async_timed
import asyncio
@async_timed()
async def cpu_bound_work():
counter = 0
for i in range(100000000):
counter += 1
return counter
@async_timed()
async def main():
task_one = asyncio.create_task(cpu_bound_work())
task_two = asyncio.create_task(cpu_bound_work())
delay_task = asyncio.create_task(delay(3))
await task_one
await task_two
await delay_task
asyncio.run(main())
输出结果
可以发现delay_task并不会和CPU密集型任务同时运行,依然是串行运行,如果想达到预期的效果,后面的会通过进程池来实现
5.2 运行阻塞API
这是第二种情况,执行任何非协程的I/O操作或执行耗时的CPU操作的函数,都被视为阻塞,例如request或者time.sleep等。
下面是在协程中错误的使用阻塞API
from util.auxiliary_function import delay,async_timed
import asyncio
import requests
@async_timed()
async def get_example_status():
return requests.get('http://www.baidu.com').status_code
@async_timed()
async def main():
task_one = asyncio.create_task(get_example_status())
task_two = asyncio.create_task(get_example_status())
task_three = asyncio.create_task(get_example_status())
await task_one
await task_two
await task_three
asyncio.run(main())
输出结果
可以发现main
的执行时间约等于三个任务的执行时间之和,所以可以证明这样的应用,并没有任何并发优势,因为request库是阻塞的。
通常大多数API都是阻塞的,如果想要实现加速,需要使用支持协程且利用非阻塞套接字的库,例如aiohttp,当然,之后也可以使用线程池。
6 手动创建和访问事件循环
某些情况下,我们希望执行自定义逻辑来完成与asyncio.run不同的任务,例如让剩下的任务完成而不是停止。
6.1 手动创建事件循环
使用asyncio.new_event_loop
方法创建一个事件循环,返回一个事件循环的实例。
该实例可以访问一个run_until_complete
的方法,该方法接受一个协程,并运行它直到完成,然后关闭并释放资源。
import asyncio
async def main():
await asyncio.sleep(1)
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
6.2 访问事件循环
有时可能需要访问当前正在运行的事件循环,可以使用asyncio.get_running_loop
函数获取当前事件循环,然后使用call_soon
,将设定一个函数在事件循环的下一次迭代中运行。
import asyncio
def call_later():
print("being called")
async def main():
loop = asyncio.get_running_loop()
loop.call_soon(call_later)
# call_soon 方法会尽可能快地将给定的回调添加到事件循环的任务队列中
# 在下一次事件循环迭代时执行该回调
await asyncio.sleep(2)
asyncio.run(main())
使用asyncio.get_running_loop()
获取事件循环,然后告知call_later
,并在下一次事件循环中迭代运行它
7 使用调试模式
asyncio提供了调试模式,帮助我们判断协程是否在CPU上占据了太多时间,或者是否在某个地方忘记了await
7.1 开启调试模式
asyncio.run(main(),debug=True)
也可以使用命令行启动调试模式
python3 -x dev test.py
也可以把PYTHONASYNCIODEBUG变量设置为1,启动调试模式
PYTHONASYNCIODEBUG=1 python3 test.py
7.2 在调试模式下运行CPU密集型代码
import asyncio
from util.auxiliary_function import async_timed
@async_timed()
async def cpu_bound_work():
counter = 0
for i in range(100000000):
counter += 1
return counter
async def main():
task_one = asyncio.create_task(cpu_bound_work())
await task_one
asyncio.run(main(), debug=True)
输出结果
默认协程花费的时间超过100毫秒,默认设置将记录警告,可以根据自己的情况来修改这个阈值,通过设置事件循环的slow_callback_duration
import asyncio
async def main():
loop = asyncio.get_running_loop()
loop.slow_callback_duration = 0.250 # 设置为250毫秒
asyncio.run(main(), debug=True)