Python 是一门上手快、优雅简洁的编程语言,其多范式、丰富的标准库和第三方库能够让编程人员把精力集中在逻辑和思维方法上,而不用去担心复杂语法、类型系统等外在因素,从而高效地达成自己的编程目标。Python 抽象层次非常高,这帮助我们更好更快地完成编程,但也屏蔽了很多细节,程序员也无法精确控制计算机底层的资源,代码性能优化就变得比较复杂。很多资深的程序员可能会觉得 Python 性能不够好,无法编写高性能的程序,其实这句话也不全对。对于计算密集型的程序,Python 可以通过扩展的形式使得核心计算直接调用其它语言(通常是 C 语言)写的包。比如说近期流行的机器学习所使用的 CUDA 技术,Nvidia 就在官网发表了一篇文章专门讲如何通过 Python 使用 CUDA 技术。还有科学计算库 Numpy,也是有代表的 Python 高性能的第三方库。如果第三方库不能满足我们,还可以通过 Cython 这种有着类 Python 语法的工具来生成 Python 的 C 语言扩展库,直接给我们的程序加速。这些技术使得 Python 也可以有高性能的计算能力,但是本身核心代码并不是用 Python 写的,所以在本文中不关注这些技术。我们在本文中主要讨论另一种情况,如何写 I/O 密集型的高性能 Python 程序。
RQData 就是一个典型的需要高 I/O 性能的产品。RQData 是米筐为专业投资者提供便利易用的金融数据 API 。用户使用 RQData 客户端工具包 rqdatac,通过网络的方式调取存在远程的金融数据(云端或本地取决于实施方式)。RQData 提供了非常多的 Python API,方便投资者调取全市场的金融数据。这些 API 包含了股票、期货、现货、期权、可转债、场内基金、风险因子、财务因子等类型,而这些数据最重要的特性就是数据量大。RQData 日常面临着每天数亿次对于总量约为 1T 数据的随机调取。这些调取所需返回的数据量往往也相当庞大,例如十年全 A 股的日线数据大约会返回 1 个 G 的数据。在这样的压力下,我们仍希望用户在使用 RQData 的时候就像浏览器下载文件一样快,这些需求在 RQData 中都是用 Python 通过 asyncio 模块完成的。接下来我们一起由浅入深地学习一下 asyncio 。
Asyncio 初识
asyncio 是 Python 3.5 中引入的一个库, 目的是方便编写网络应用程序。一个简单的以 RQData 服务端为例的网络处理的核心代码基本如下(简化了 API 的业务逻辑):
上面的代码建构了一个简单的服务端应用,它接收到用户的连接后,直接返回用户输入的数据。这个程序非常简单,却能体现出 asyncio 的魅力。我们构建的程序非常像传统的网络程序。和传统的网络程序相比,我们改动了三处:
- 用了 async 关键字定义了一个协程函数,而不是一个普通的函数。
- 相比于传统的阻塞的函数调用 , 在调用 async 关键字定义的函数前加入了 await 。
- 没有使用 socket 定义的方法,而是换成了 asyncio 中 EventLoop 定义的 async 协程函数。
仅仅多了 async 和 await 两个关键字,我们就享受了协程的优势。协程让我们不用担心过多的线程导致操作系统调度占用了太多的 CPU 时间,我们的程序可以运行在一个 CPU 上,享受 CPU 的高速缓存,减少 CPU 缓存同步的开销,而且我们的代码非常容易理解,几乎和传统的网络程序没有什么区别。
异步与协程
asyncio 全称 asynchronous I/O ——异步输入输出。相对于同步,异步函数会在调用时不等待 I/O 操作完成,I/O 结果通过回调的方式交给调用者。举个打电话的例子,如果对方忙线,同步函数会一起挂起,直到对方接听了我的电话;异步函数则是留言到语音信箱,通知对方在完成了上一通电话后,再给