GIL正在杀死I / O绑定线程

时间:2016-04-02 20:11:37

标签: python multithreading cpython gil

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

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

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

Clip of visualization

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

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

如果这个假设是正确的,那么我意识到我可以通过分离一个单独的进程来完成图像处理工作来解决这个问题。然而,这会使代码变得复杂并使其变得更加丑陋,所以如果可能的话,我想避免使用它。

因此:有没有办法避免这种明显的GIL问题?我可以这样做,以便调度员线程不会轻易放弃GIL,允许它处理处理窗口之间的一些积压吗?可以对GIL CPU窗口进行调整",或者我可以指定一些较低的" GIL优先级"到CPU绑定的线程或类似的东西?还有其他方法吗?或者我可能完全误解了这个问题?

很抱歉啰嗦,但我无法用更简洁的方式描述这种情况。

2 个答案:

答案 0 :(得分:1)

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

我尝试重新实现线程池,以便调度线程可以简单地发布请求而无需与工作线程一起锁定步骤,​​这似乎使问题完全消失了。在一段时间的图像处理中可视化请求调度现在显示没有任何减速。据推测,然后,两个线程之间的GIL切换为第三个受CPU限制的线程创建了一个更大的窗口,以便在更长的时间内抢夺它。

我想,要学到的教训是,当前的CPython(我在服务器上使用3.4.2)似乎可以很好地混合I / O绑定和CPU绑定线程,但两个或多个线程相互锁定的线程可能会受到CPU绑定线程的限制。

答案 1 :(得分:0)

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

如果你已经编写了图像处理工具并用Cython包装它,那么保留在同一个线程中的唯一选择是存在,那么你可以使用nogil选项在图像处理过程中释放GIL。

如果您打算让网站更加强大,您可以使用Celery来管理您的工作人员。从长远来看,通过与管理Web I / O的流程分开管理更长时间运行的任务,您的网站肯定会得到帮助,但它需要您在简单的Web流程之上设置一些额外的基础架构。