原标题:Python3异步编程

Python3.4之后引入asyncio标准库,并在3.5中提供原生语法支持,为编写异步程序提供了高效且优雅的方法。对于编写爬虫和httpserver的这类IO密集型应用,asyncio的表现非常亮眼。

asyncio基于协程实现,至于为什么不用进程or线程实现并发,忽略内核陷入开销以及GIL,进程与线程依赖操作系统调度,调度开销高,调度方式也不一定与应用适配。

Coroutine协程

协程是可以暂停和恢复的用户态“线程”。但协程的定义并不十分明确,也有多种实现。但总的来讲都是基于生成器,所以为了理解协程,先简单介绍一下python生成器:

1.栈帧PyFrameObject保存代码的信息和上下文

2.栈帧拥有自己的数据栈和block栈,解释器可以中断和恢复栈帧

3.python将函数编译成字节码时,碰到yield语句,标记它为生成器函数

4.程序调用生成器函数时,python创建生成器对象

5.所有调用特定生成器函数得到的生成器对象都指向同样的代码。但每个生成器对象都有独立的栈帧

6.调用send或next方法,由gen_send_ex函数执行系列操作、修改生成器状态,然后调用PyEval_EvalFrameEx执行字节码(如果代码块为空或调用栈为空,抛出StopIteration异常)

EventLoop 事件循环

事件循环是asyncio的核心,它负责:

1.注册、执行以及取消超时调用

2.为各种通信创建client和server通道

3.启动子程序和并创建与外部程序通信的通道

4.将耗时任务委托给线程池

获取事件循环:

asyncio.get_event_loop_policy获取事件循环策略,asyncio.get_event_loop获取事件循环.

设置事件循环:

asyncio.set_event_loop_policy设置事件循环策略,asyncio.set_event_loop设置事件循环

以uvloop为例:


输出:


开启事件循环:

AbstractEventLoop.run_forever开启事件循环直到stop方法被调用

AbstractEventLoop.run_until_complete开启事件循环,如果参数是协程对象,ensure_future会将协程封装为Task。函数返回Futures的结果或者抛出相应的异常。

关闭事件循环:

AbstractEventLoop.stop如果run_forever正在运行,对当前批次执行callback后退出。

run_forever再次被调用时,继续执行。

AbstractEventLoop.close强制关闭事件循环,抛弃待处理的回调。并且关闭之后不可逆转。

Future

封装callable的异步执行,Task的基类,非线程安全。

Task任务

Task是Future的子类,负责在事件循环中执行协程。事件循环同一时间只执行一个task(其他线程中的任务可能可以“并行”)。当task等待其他future完成时,事件循环执行新的task(并发)。

使用asyncio.ensure_future函数或者AbstractEventLoop.create_task方法创建task,Task非线程安全。

对于非线程安全问题,一个event_loop只在一个线程中运行,所以只需要担心event_loop之外的情况,asyncio.run_coroutine_threadsafe与loop.call_soon_threadsafe可以应对。

Crawler

接下来我们基于asyncio和aiohttp编写一个爬虫抓取豆瓣电影top250(隐去异常处理,下同):


COSTTIME: 0.25513544082641604S

线程池版本:


COSTTIME: 0.4078077793121338 S

这个对比虽不够严谨,但也可以看出asyncio的异步性能,特别是在并发量越来越大的时候,python可以轻松开启上万的协程,而开启上万线程操作系统可能会不堪重负。

结语

上面我们介绍了协程和asyncio标准库,并编写了一个简单的网络爬虫,对比协程和多线程版本。实际上对于HTTPSERVER,Sanic+uvloop能创造出惊人的性能,详见uvloop:Blazing fast Python networking(

https://magic.io/blog/uvloop-blazing-fast-python-networking/)