背景
编程时遇到的阻塞任务一般有两类:
- 等待 I/O 就绪(I/O 密集型);
- 耗时的计算工作(CPU 密集型)。
遇到这类任务,通常首选考虑是否可以优化操作(主要是针对第 2 种情况),将阻塞限制在可以接受的范围内,若不行则考虑使用多线程或多进程,将阻塞操作交由其他线程(例如 .NET APM,.NET 异步编程模型使用线程池来异步执行任务)或进程(fork/exec、任务队列,或者异步委托第三方服务 API)去异步处理,然后在操作结束后取回结果。对于第 1 种情况,使用操作系统支持的非阻塞 I/O 来提供异步支持是最理想的方式, 这也是 Tornado 的核心工作原理。
由于 Tornado 工作模型的原因,通过异步库(指由系统级非阻塞 I/O 来提供支持的库)来处理 I/O 密集型操作才是 Tornado 的正确工作方式,否则一个任务出现阻塞(或者执行时间过长)就会导致其他请求不能被及时处理。遇上没有异步库支持的 I/O 操作(比如磁盘 I/O 操作,Linux 不能很好地提供异步支持)以及 CPU 密集型操作,在 Tornado 中一般我们可以简单地用下面两种方式进行处理:
- 将阻塞操作委托给 futures 模块的 ThreadPoolExecutor/ProcessPoolExecutor 去执行;
- 使用 tornado + celery(RabbitMQ 或 Redis 做 Broker,totoro我个人弄的一个 tornado+celery 适配库,支持 RabbitMQ 和 Redis),将阻塞操作委托给 celery 执行。
NOTE:python2 需要单独安装 futures 模块(pip install futures),python3 自带不需要单独安装。
这篇笔记主要是记录在使用 ProcessPoolExecutor 去执行时遇到的一些问题和最终解决方法。
ThreadPoolExecutor/ProcessPoolExecutor
如何选择 ThreadPoolExecutor 和 ProcessPoolExecutor
由于 Python GIL 的原因,利用多线程(“单进程,多线程”) 去处理 CPU 密集型任务并不能有效地利用多核,提高性能。在处理 I/O 密集型任务时,由于遇到 I/O 阻塞时线程会主动释放 GIL,多线程才能明显提高性能。
基于上述原因,在 Tornado 中区分一个任务是 CPU 密集型还是 I/O 密集型很重要,前者选择 ProcessPoolExecutor,后者选择 ThreadPoolExecutor 理论上是正确的。