如何让 Python 处理速度翻倍?内含代码( 二 )


原理篇根据 wikipedia 的定义,协程是一个无优先级的子程序调度组件,允许子程序在特点的地方挂起恢复 。所以理论上,只要内存足够,一个线程中可以有任意多个协程,但同一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源 。协程是为了充分发挥异步调用的优势,异步操作则是为了避免 IO 操作阻塞线程 。
知识准备在了解原理前,我们先做一个知识的准备工作 。
1)现代主流的操作系统几乎都是分时操作系统,即一台计算机采用时间片轮转的方式为多个用户服务,系统资源分配的基本单位是进程,CPU 调度的基本单位是线程 。
2)运行时内存空间分为变量区,栈区,堆区 。内存地址分配上,堆区从低地到高,栈区从高往低 。
3)计算机执行时一条条指令读取执行,执行到当前指令时,下一条指令的地址在指令寄存器的 IP 中,ESP 寄存值指向当前栈顶地址,EBP 指向当前活动栈帧的基地址 。
4)系统发生函数调用时操作为:先将入参从右往左依次压栈,然后把返回地址压栈,最后将当前 EBP 寄存器的值压栈,修改 ESP 寄存器的值,在栈区分配当前函数局部变量所需的空间 。
5)协程的上下文包含属于当前协程的栈区和寄存器里面存放的值 。
事件循环在 Python3.3 中,通过关键字 yield from 使用协程,在 3.5 中,引入了关于协程的语法糖 async 和 await,我们主要看 async/await 的原理解析 。其中,事件循环是一个核心所在,编写过 js 的同学,会对事件循环 Eventloop 更加了解, 事件循环是一种等待程序分配事件或消息的编程架构 (维基百科) 。在 python 中,asyncio.coroutine 修饰器用来标记作为协程的函数, 这里的协程是和 asyncio 及其事件循环一起使用的,而在后续的发展中,async/await 被使用的越来越广泛 。
async/awaitasync/await 是使用 python 协程的关键,从结构上来看,asyncio 实质上是一个异步框架,async/await 是为异步框架提供的 API 已方便使用者调用,所以使用者要想使用 async/await 编写协程代码,目前必须机遇 asyncio 或其他异步库 。
Future在实际开发编写异步代码时,为了避免太多的回调方法导致的回调地狱,但又需要获取异步调用的返回结果结果,聪明的语言设计者设计了一个 叫 Future 的对象,封装了与 loop 的交互行为 。其大致执行过程为:程序启动后,通过 add_done_callback 方法向 epoll 注册回调函数,当 result 属性得到返回值后,主动运行之前注册的回调函数,向上传递给 coroutine 。这个 Future 对象为 asyncio.Future 。
但是,要想取得返回值,程序必须恢复恢复工作状态,而由于 Future 对象本身的生存周期比较短,每一次注册回调、产生事件、触发回调过程后工作可能已经完成,所以用 Future 向生成器 send result 并不合适 。所以这里又引入一个新的对象 Task,保存在 Future 对象中,对生成器协程进行状态管理 。
Python 里另一个 Future 对象是 concurrent.futures.Future,与 asyncio.Future 互不兼容,容易产生混淆 。区别点在于,concurrent.futures 是线程级的 Future 对象,当使用 concurrent.futures.Executor 进行多线程编程时,该对象用于在不同的 thread 之间传递结果 。
Task上文中提到,Task 是维护生成器协程状态处理执行逻辑的的任务对象,Task 中有一个 _step 方法,负责生成器协程与 EventLoop 交互过程的状态迁移,整个过程可以理解为:Task 向协程 send 一个值,恢复其工作状态 。当协程运行到断点后,得到新的 Future 对象,再处理 future 与 loop 的回调注册过程 。
Loop在日常开发中,会有一个误区,认为每个线程都可以有一个独立的 loop 。实际运行时,主线程才能通过 asyncio.get_event_loop() 创建一个新的 loop,而在其他线程时,使用 get_event_loop() 却会抛错 。正确的做法为通过 asyncio.set_event_loop(),将当前线程与 主线程的 loop 显式绑定 。
Loop 有一个很大的缺陷,就是 loop 的运行状态不受 Python 代码控制,所以在业务处理中,无法稳定的将协程拓展到多线程中运行 。
总结

如何让 Python 处理速度翻倍?内含代码

文章插图
 
实战篇介绍完概念和原理,我来看看如何使用,这里,举一个实际场景的例子,来看看如何使用 python 的协程 。
场景外部接收一些文件,每个文件里有一组数据,其中,这组数据需要通过 http 的方式,发向第三方平台,并获得结果 。
分析由于同一个文件的每一组数据没有前后的处理逻辑,在之前通过 Requests 库发送的网络请求,串行执行,下一组数据的发送需要等待上一组数据的返回,显得整个文件的处理时间长,这种请求方式,完全可以由协程来实现 。


推荐阅读