GIL 正在杀死 I/O-bound 个线程

GIL is killing I/O-bound thread

我有一个主要用 Python 编写的网站。处理 Python 绑定请求的 Python 进程有一个调度线程,它从 Web 服务器获取请求并将它们简单地调度到线程池进行处理。因此,在调度线程中完成的工作非常简单;它只是通过 Unix 套接字读取请求并在线程池上做一些同步。正常情况下每秒可以发送2000多个请求

然而,有时会发生一些奇怪的事情。该网站的一部分对上传的文件进行了一些图像处理,由于图像处理算法完全在 Python 中编写,因此需要一些时间,在 CPU 上旋转。在较大的图像上,可能需要 5 秒或更长时间。不过,这本身就很好;奇怪的是,当它进行处理时,调度线程的吞吐量急剧下降。当图像处理器 运行ning 时,调度吞吐量下降到每秒大约 20-30 个请求 -- 几乎两个数量级!

这给我带来了一些小麻烦,因为在繁忙时间,Python 处理程序每​​秒接收大约 50-100 个请求,因此无法跟上。对于需要大约 3 秒或更长时间的图像处理请求,缓冲区开始填满,因此 Web 服务器被迫开始丢弃发往 Python.

的请求

我写了一个可视化工具来帮助调试问题,this image(上面裁剪)演示了正在发生的事情。每个请求的调度被绘制为沿 X 轴的一条线,每个后续请求被绘制在随后的 Y 坐标上。每条垂直网格线表示一秒钟,红色网格线是我的 HTTP 服务器记录它开始丢弃请求的地方。可以清楚地看到调度速度在这之前大约 2.5 秒减慢了很多,与访问日志相比,这是图像处理器启动的地方。

我的假设是,这是因为 CPU 绑定的图像处理器线程正在占用 GIL,并且调度程序必须等待某些特定的 "processing window" 完成,直到 CPU-bound 线程自愿释放其他线程的 GIL 到 运行。另一方面,调度程序线程每次进入阻塞系统调用时都会释放 GIL,然后必须等待另一个完整的处理 window 完成才能处理下一个请求。

如果这个假设是正确的,那么我意识到我可以通过分叉一个单独的进程来完成图像处理工作来解决这个问题。但是,这会使代码复杂化并变得更难看,所以我想尽可能避免这种情况。

因此:有什么方法可以避免这个明显的 GIL 问题吗?我能否做到这一点,以便调度程序线程不会轻易放弃 GIL,从而允许它处理处理 windows 之间的一些积压工作? GIL CPU window 可以是 "tweaked",或者我可以将一些较低的 "GIL priority" 分配给 CPU 绑定线程或类似的东西吗?还有其他办法吗?还是我完全误解了这个问题?

抱歉啰嗦,但我真的想不出更简洁的方式来描述这种情况。

相信你对问题的认识是正确的。 对我来说,解决这个问题最直接的方法是用多处理模型替换线程模型。与简单地生成一个单独的进程相比,在同一进程中避免 GIL 问题要复杂得多。 在 python 中,没有直接的方法(据我所知)更改线程的优先级。

如果您已经编写了图像处理工具并使用 Cython 对其进行了封装,那么留在同一个线程中的唯一选择是存在,那么您可以使用 nogil 选项在图像处理发生时释放 GIL。

如果您打算使网站更加健壮,您可以使用 Celery 来管理您的员工。从长远来看,运行 您的网站肯定会因为将更长的 运行 宁任务与管理 Web I/O 的进程分开管理而得到帮助,但这需要您设置一些在简单的 Web 流程之上增加基础设施。

我确实设法弄清楚了为什么会这样。事实证明,与其说阻塞系统调用本身是个问题,不如说是线程池实现的那一部分让调度线程等待,直到工作线程可以确认它已经接受了请求(出于会计原因,基本上)通过发信号通知调度线程等待的条件变量。

我尝试重新实现线程池,这样分派线程就可以简单地 post 请求,而不必与工作线程同步工作,这似乎使问题完全消失了.可视化图像处理期间的请求调度现在显示没有任何减速。那么,据推测,GIL 在两个线程之间的切换为第三个 CPU 绑定的线程创建了一个更大的 window 以抢夺它更长的时间。

那么,我想,要吸取的教训是,当前的 CPython(我在服务器上使用 3.4.2 运行 这个)似乎可以很好地混合使用 I/O-bound 和CPU 绑定的线程,但是两个或多个彼此同步工作的线程可能会被 CPU 绑定的线程饿死。