asyncio 是用来编写并发程序的库。在爬虫、客户端应用等开发场景中, 我们经常会需要将多个网络请求并行化来提高程序性能,而 asyncio 框架正好可以很方便的帮助我们实现这个需求。
我最早使用 asyncio 是在 2015 年、一个音乐播放器项目中。最开始用的时候, 对这东西不是很了解,只知道它可以让多个网络请求一起发送出去,那时侯, 代码基本都是从 stackoverflow 和网上各大博客中中摘抄下来,目标是能正常工作就行。
但随着项目变大、疑问逐渐变多,我觉得自己需要了解 asyncio 背后的运行原理。 于是在 2018 年初,我开始学习 asyncio,断断续续的,到现在快一年,自己对它终于有了个整体认知,于是想以文字形式来记录下自己对 asyncio 的理解。
文字记录也有很多形式和风格,我之前的文字风格大多是流水帐、自言自语。asyncio 这块知识,我想写成一系列的文章。一方面是想挑战下自己的写作能力; 另一方面,目前网上 asyncio 相关资料也不是特别完整,想着把自己了解的都拿出来和大家分享下。
学习 asyncio 的过程特别艰辛,在学习它的时候,我发现自己有许多基础知识没有掌握, 所以经常出现这样一个情况:我本来正在学习 asyncio, 而 asyncio 里面用到了 A 库, 于是我就去理解 A,但 A 又依赖 B,于是我又得学 B,B 还可能会依赖 C... 我碰到过很多这样的链式问题(和自己基础差也有一定关系),下面列出来的是我自己记忆比较深刻的问题:
基础知识相关问题
实现相关问题
时至今日,对于上面提到的大部分问题,我都有了一些自己的想法。 我想把自己的理解记录在这系列文章中。如果你恰好也对这些问题感兴趣,就请继续往下阅读把 ~
在这个系列文章中,我把重点放在 asyncio 背后的原理以及相关的基础知识。 至于 asyncio 的使用姿势(最佳实践),我不太会涉及,也确实没有太多相关经验, 毕竟自己也还是个学习者,也没有在生产环境大量的使用它。
在这篇文章中,我们会编写一个程序来简单模拟 asyncio 的行为。 读完这篇文章,读者应该会对下面两个问题有(大概的)认知:
看到这里,不知道读者会不会有个疑问:协程到底是什么?如果有的话, 我觉得读者可以先把这个疑问藏在心中。我们暂时不需要纠结这个概念本身, 在之后的内容或者文章中,我们会尝试给出几种解释。目前, 我们只需对它有个整体的认识即可。
在 Python 中,我们可以用 async/await 语法来声明一个协程。 我们先来看一个实际的使用示例:用 asyncio 运行一个 hello world 协程
import asyncio
async def hello_world():
print('hello world')
loop = asyncio.get_event_loop()
loop.run_until_complete(hello_world())
运行这段代码,我们可以在终端看到程序打印出了 "hello world",说明我们的 `hello_world()` 协程已经执行完毕了。
我们知道,协程不能像普通函数那样直接执行。比如下面这段代码,它就没有打印出 hello world。 并且,hello_world
函数的返回值也不是 None, 它是一个 coroutine 对象。
>>> async def hello_world():
... print('hello world')
...
>>> coro = hello_world()
>>> type(coro)
接着进行一些探索,我们可以使用 dir
方法来查看一个对象有哪些方法:
>>> dir(coro)
['__await__', '__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'cr_await', 'cr_code', 'cr_frame', 'cr_origin', 'cr_running', 'send', 'throw']
忽略 __
开头的 magic 方法,我觉得 send 方法比较顺眼,就调用 coroutine 对象的 send 方法试试把:
>>> coro.send(None)
hello world
Traceback (most recent call last):
File "", line 1, in
StopIteration
很巧诶,"hello world" 被成功的打印出来了,我们似乎达到了我们的目标: 不用 asyncio 也能运行协程了。只不过,程序抛了一个 StopIteration 异常。 但异常可以很容易的处理掉嘛,try...except...
一下就行了。
>>> coro2 = hello_world()
>>> try:
... coro2.send(None)
... except StopIteration:
... pass
...
hello world
Cheers, 我们已经成功的模拟了 asyncio 的行为,将 coro 协程运行起来了。
asyncio 还可以运行多个协程
>>> import asyncio
>>> loop = asyncio.get_event_loop()
>>> async def job1():
... print('hello, job1)
...
>>> async def job2():
... print('hello, job2')
...
>>> loop.create_task(job1())
:1>>
>>> loop.create_task(job2())
:1>>
>>> try:
... loop.run_forever()
... except KeyboardInterrupt: # 处理 Ctrl-C
... pass
...
hello, job1
hello, job2
^C
>>>
仍然,不依赖 asyncio,我们自己实现下。我们这次封装一个 run 函数,用来运行多个协程:
>>> coro1 = job1()
>>> coro2 = job2()
>>>
>>> def run(*coros):
... for coro in coros:
... try:
... coro.send(None)
... except StopIteration:
... pass
...
>>> run(coro1, coro2)
hello, job1
hello, job2
Cool, it works!
从上面几个例子来看,asyncio 的原理似乎很简单,就是帮我们执行一下协程的 send 方法嘛。
等等,你知道 Python 的生成器么,它似乎也有个 send
方法,而且效果很类似:
>>> def hello_world():
... print('hello, generator')
... yield
...
>>> g = hello_world() # 生成一个生成器对象
>>> g.send(None) # 启动生成器
hello, generator
与协程不同的时,在生成器这个例子里,我们调用生成器的 send 方法时, 它并不会抛出异常。但如果我们再次执行一下 send 方法,它会怎样呢?
>>> g.send(None)
Traceback (most recent call last):
File "", line 1, in
StopIteration
它也会抛出 StopIteration
的异常 ~ 行为非常相似!
ummm... 大人,此事必有蹊跷!
在本篇文章中,我们讲述了学习 asyncio 时可能会遇到的一些基础知识; 看了几个简单的 asyncio 使用示例;并且自己动手编写了一个 run 函数来运行多个协程,简单的模拟了 asyncio 的行为。 最后,我们还发现:Python 的协程和生成器行为非常类似,但是它们具体是什么关系呢? 还请听下回分解。