在一项网络 IO 繁重的 Python 项目中,我大量使用了 gevent 的协程模型加速网络 IO ,效果比 Python 传统的线程池好很多。然而在后续数据预处理的部分遇到了问题。

我的项目结构是一个经典的单 provider 、多 consumer 模型,provider 负责读取一个超大文件(大于 8 Gb)并写入队列,然后多个 consumer 从队列读取数据并进行处理。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

q = Queue()

def provider():
with open('<file path>', w) as f:
for line in f:
q.put(line)

def consumer():
data = q.get()
...

tasks = [gevent.spwan(provider)] + [gevent.spwan(consumer) for _ in range(works_num)]
gevent.joinall(tasks)

然而在运行的时候,程序总是阻塞在文件读操作,即 provider 上。也就是说,必须等待文件读入队列这个事件结束后,消费者才能从队列获取数据进行处理,完全无法满足并行(边读边处理)的需要。

经过调研,我发现在 Linux 系统下,和网络 IO 不同,文件 IO 一直是 ready 状态,不存在不可达或需要等待的问题。因此 Linux 会使一个文件 IO 线程不停进行 write/read 。由于这个文件 IO 的过程阻塞了执行的线程,进而全部阻塞了该线程中的所有协程。这就是我遇到了读取/写入一个文件,却导致所有协程阻塞的原因。

一篇文章指出,如果要实现文件 IO ,不得不至少实现多线程,没有其它选择——即使我们必须要面对 Python 臭名昭著的 GIL 锁。

gevent 的一个 issue 也说明了为何不支持非阻塞式的文件 IO。

我将程序改为线程池实现后,就成功解决了这个问题,实现了边读边处理的并行需求。

参考文章 https://blog.csdn.net/u014609111/article/details/118181367