文章目录
1. 前言
引用廖雪峰老师的话
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配
,多线程和多进程只是解决这一问题的一种方法。
另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
这就是为什么需要异步IO
。在一个线程中,在多个协程之间切换,充分实现CPU的高效使用。
先讲讲同步/异步和阻塞/非阻塞:建议看 知乎 怎样理解阻塞非阻塞与同步异步的区别? YI LU的回答
同步和异步关注的是消息通信机制;阻塞和非阻塞关注的是程序在等待调用结果时的状态。
同步
。就是在发出一个调用时,没得到结果之前,该调用就不返回。但是一旦调用返回就得到返回值了,调用者主动等待这个调用的结果异步
。就是在发出一个调用时,这个调用就直接返回了,不管返回有没有结果。当一个异步过程调用发出后,被调用者通过状态,通知来通知调用者,或者通过回调函数处理这个调用
-阻塞
。调用结果返回之前,当前线程会被“挂起”,直到结果返回,才会恢复执行。非阻塞
。调用结果返回之前,当前线程可以去做其他事情,但是要时不时地检查是否有结果返回了。
网络上的例子
老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞);立等就是阻塞了老张去干别的事,老张得一直主动的看着水开没,这就是同步
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞);老张去看电视了,这就是非阻塞了,但是老张还是得关注着水开没,这也就是同步了
3 老张把响水壶放到火上,立等水开。(异步阻塞);立等就是阻塞了老张去干别的事,但是老张不用时刻关注水开没,因为水开了,响水壶会提醒他,这就是异步了
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞);老张去看电视了,这就是非阻塞了,而且,等水开了,响水壶会提醒他,这就是异步
举个例子,看看之前的同步编程。
do_something()
f = open('test.txt')
result = f.read() # 线程停在此处,等待IO的结果,这段时间CPU啥事也没干,浪费了CPU资源
do_othner_something()
f.close()
而使用异步编程,就是当CPU空闲的时候,可以转而去做其他事情,这岂不美哉。
就好比拿水桶去接水。
同步编程:把桶放在水龙头下边,打开水龙头。这期间你就等着水接满,啥也不干。
异步编程:把桶放在水龙头下边,打开水龙头。不用等水接满,水满了自然会有动静告诉你已经满了,这期间你可以接第二桶水、第三桶水。
2. 核心概念
2.1 coroutine(协程)
协程,也叫做微线程。在一个线程之间,是一种用户态上下文切换技术,实现CPU的高效使用。在协程获得返回值之前可以暂停执行,将执行权转交给其他协程一段时间。
协程函数:用async def
定义的函数。
协程对象:
协程的实现:
yield/yield from
语法。asyncio.coroutine
装饰器。async/await
语法。推荐使用。
如下代码所示,定义了一个协程函数main
。
>>> import asyncio
>>> async def main():
... print('hello')
... await asyncio.sleep(1)
... print('world')
>>> asyncio.run(main())
hello
world
当使用asycio.run(main())
启动协程时,会发现打印hello之后隔1s会打印world。当然,现在看来这个同步的代码是一样(直接使用time.sleep(1)
不也是一样的效果么?)。这是因为目前,没有多余的协程可以切换,所以看不出效果。
注意,当我们使用main()时,并没有执行协程。调用协程函数只是得到了一个协程对象(),单并不会执行这个协程对象。因此,要使用await关键字来执行awaitable(如,协程对象、Tasks、Futures等)
正确的协程函数定义
async def f(x):
y = await z(x) # OK - `await` and `return` allowed in coroutines
return y
async def g(x):
yield x # OK - this is an async generator
async def m(x):
yield from gen(x) # No - SyntaxError,在async def 中,不能使用yield from
def m(x):
y = await z(x) # Still no - SyntaxError (no `async def` here)
return y
2.2 awaitable(可等待对象)
能在 await
表达式中使用的对象。可以是 coroutine
或是具有 __await__()
方法的对象。
主要的awaitable
对象:
- 协程对象
Tasks
Futures
2.3 Event loop(事件循环)
事件循环是每个 asyncio
应用的核心。 事件循环会运行异步任务和回调,执行网络 IO 操作,以及运行子进程。
开发者通常应当使用高层级的 asyncio 函数,例如
asyncio.run(),最好不要引用
loop`对象或调用其方法。
await
关键字的意思是,当遇到await
的 awaitable
对象,表明需要等待awaitable
执行完成之后,才能继续执行此协程的剩余部分。遇到await 表示需要中断当前协程的执行(例如,遇到耗时的IO操作,不能白白浪费CPU
啊),转而去执行其他协程了。
2.4 Tasks
用来并发地调度协程。 使用asyncio.create_task()
可以将一个协程包装成一个Task
,并自动将该协程的Task
加入event loop
中。
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
输出如下。用了3秒,并不是异步的。
started at 17:13:52
hello
world
finished at 17:13:55
现在我们把协程搞成Task。
async def main():
task1 = asyncio.create_task(
say_after(1, 'hello'))
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1
await task2
print(f"finished at {time.strftime('%X')}")
输出如下:用时2s,表明是异步的了。
started at 17:14:32
hello
world
finished at 17:14:34
创建一个Task
# 第1种方式 ,推荐使用
asyncio.create_task()
# 第二种方式
loop = asyncio.get_event_loop()
loop.create_task()
# 第三种方式
asyncio.ensure_future()
2.5 Futures
一个 Future
是一个低级的awaitable
对象,用来代表一个异步运算的最终结果。线程不安全。
Future
是一个 awaitable
对象。协程可以等待Future
对象直到它们有结果或异常或被取消。
3. 简单使用
#!/usr/bin/env python3
# countasync.py
import asyncio
async def count():
print("One")
await asyncio.sleep(1)
print("Two")
async def main():
await asyncio.gather(count(), count(), count())
if __name__ == "__main__":
import time
s = time.perf_counter()
asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"{__file__} executed in {elapsed:0.2f} seconds.")
输出结果如下:
$ python3 countasync.py
One
One
One
Two
Two
Two
countasync.py executed in 1.01 seconds.
从上面的输出顺序,我们也能发现其实异步执行的了。上面的代码,将3个count()
(其实是3个协程对象)加入了event loop
。当第一个协程遇到await asyncio.sleep(1)时,会将执行权交还给event loop,让event loop 安排一个协程获取一段时间的执行权。
如果是同步编程:
#!/usr/bin/env python3
# countsync.py
import time
def count():
print("One")
time.sleep(1)
print("Two")
def main():
for _ in range(3):
count()
if __name__ == "__main__":
s = time.perf_counter()
main()
elapsed = time.perf_counter() - s
print(f"{__file__} executed in {elapsed:0.2f} seconds.")
输出如下:
$ python3 countsync.py
One
Two
One
Two
One
Two
countsync.py executed in 3.01 seconds.
async def
定义了一个协程函数。
await
关键词将执行权交还给event loop
。如下代码所示,在g()
中遇到 await f()
,那么会将执行权交还给event loop
,让其安排其他协程执行,并将g()
挂起,直到得到了f()
的返回值。
async def g():
# Pause here and come back to g() when f() is ready
r = await f()
return r
在Python 3.7
以上的版本中,事件循环的整个管理由一个函数隐式处理(asyncio.run(main())
)
Python 3.7
中引入的asyncio.run()
负责获取事件循环,运行任务直到将其标记为完成,然后关闭事件循环。
使用get_event_loop()
,可以更轻松地管理asyncio
事件循环。 典型的模式如下所示:
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
在较早的示例中,您可能会看到loop.get_event_loop()
随处可见,但是除非您特别需要微调对事件循环管理的控制,否则asyncio.run()
对于大多数程序而言就足够了。
以下是事件循环的一些要点
:
在将协程安排进事件循环之前,协程并不会做太多事情
。
通过main()
这种调用方式,只会得到一个协程对象,并不会真正的执行。
>>> import asyncio
>>> async def main():
... print("Hello ...")
... await asyncio.sleep(1)
... print("World!")
>>> routine = main()
>>> routine
<coroutine object main at 0x1027a6150>
通过asyncio.run()
在事件循环中调度main协程的执行。
>>> asyncio.run(routine)
Hello ...
World!
默认情况下,异步IO事件循环在单个线程和单个CPU内核上运行。 通常,在一个CPU内核中运行一个单线程事件循环绰绰有余。 还可以跨多个内核运行事件循环。
事件循环是可插拔的。
也就是说,如果您确实需要,可以编写自己的事件循环实现,并使它运行相同的任务。 这在uvloop软件包中得到了很好的演示,该软件包是Cython中事件循环的实现。
协程与多线程/多进程相比有何优劣?
- 协程执行效率极高,没有线程切换,因此没有线程切换的开销。
- 通常,协程只在一个线程中的不同代码块进行切换,不需要多线程的锁机制,效率高。
由于协程只使用1个线程,那岂不是浪费了多核CPU?
可以使用多进程+协程。充分利用CPU资源。
4. 参考文献
[1] 这篇写的真的很不错 Python异步IO之协程(一):从yield from到async的使用
[2] Async IO in Python: A Complete Walkthrough
[3] 知乎 怎样理解阻塞非阻塞与同步异步的区别? YI LU的回答
[4] Python 官方文档 Asynchronous I/O
[5] 小破站 2小时学会python asyncio【花39大洋买的课程】
[6] 阮一峰的网络日志 Python 异步编程入门
[7] 廖雪峰的官方网站 asyncio