繁体   English   中英

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

[英]GIL is killing I/O-bound thread

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

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

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

可视化剪辑

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

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

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

因此:有什么办法可以避免这种明显的 GIL 问题? 我可以这样做,以便调度程序线程不会那么容易地放弃 GIL,允许它在处理窗口之间处理一些积压吗? GIL CPU 窗口是否可以“调整”,或者我是否可以为 CPU 绑定线程或类似的东西分配一些较低的“GIL 优先级”? 还有其他方法吗? 或者我可能完全误解了这个问题?

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

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

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

那么,我想要吸取的教训是当前的 CPython(我在运行它的服务器上使用 3.4.2)似乎可以很好地混合 I/O-bound 和 CPU-bound 线程,但是这两个或更多以锁步方式工作的线程可能会被 CPU 密集型线程耗尽。

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

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

如果您打算使网站更强大,您可以使用Celery管理您的员工。 从长远来看,将运行时间更长的任务与管理 Web I/O 的进程分开管理,肯定会对您的网站有所帮助,但这需要您在简单的 Web 进程之上设置一些额外的基础设施。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM