如果存在GIL,Python中的多线程有什么意义?

64

据我所知,全局解释器锁(GIL)使得无法让每个线程单独利用一个核心。

这是一个基本问题,但是,如果使用线程的代码与普通程序具有相同的速度,那么threading库的作用是什么呢?似乎毫无用处。


5
它可以用于解除主线程的阻塞(例如 GUI 应用程序或类似应用)。如果您想使用多个核心,建议尝试多进程处理(https://docs.python.org/3.7/library/multiprocessing.html)。 - user3188639
1
尝试回答这个问题。简短的回答是:它可能有用,但也许不是你想象中的那样。由于GIL的存在,一次只能有一个线程处理Python,这意味着线程化程序仍然按顺序运行。multiprocessing库更有助于您所寻找的内容,因为它可以实际上生成利用各个核心的进程。 - bearplane
感谢@questionable_code和@Tom的帮助。我正在研究多进程,可能需要使用它。我仍然好奇为什么他们甚至有threading库。似乎更多是为了代码组织。 - coolster1278
1
看看这些(http://dabeaz.com/python/UnderstandingGIL.pdf)和这些(http://www.dabeaz.com/python/GIL.pdf)演讲,非常有趣。似乎多线程程序在1个核心上比在2或4个核心上运行得更快。这些演讲相当古老(2010年),并且提到了Python 3.x中的新GIL,但我没有尝试过。 - user3188639
4个回答

34

在某些情况下,应用程序可能无法充分利用甚至一个核心,使用线程(或进程)可以帮助做到这一点。

想象一个典型的Web应用程序。它接收来自客户端的请求,对数据库进行一些查询并将数据返回给客户端。鉴于IO操作比CPU操作慢一个数量级的时间,大多数时候这样的应用程序正在等待IO完成。首先,它等待从套接字读取请求。然后它等待将请求写入打开到数据库的套接字中。然后等待来自数据库的响应以及响应被写入客户端套接字。

等待IO完成可能需要处理请求的时间的90%(或更多)。当单线程应用程序在等待IO时,它只是没有使用核心,该核心可供其他线程在单个核心上执行。

在这种情况下,当一个线程等待IO完成时,它释放GIL,另一个线程可以继续执行。


2
那么,可以得出结论,当编写IO绑定程序时,Python线程模块非常有用。这是正确的吗?还有其他需要考虑的情况吗? - ahfx
1
@ahfx 还有很多其他的,甚至包括一些CPU密集型的。 - Bharel

13
尽管存在GIL,但线程库的工作非常出色。
在我解释之前,你应该知道Python的线程是真正的线程 - 它们是运行Python解释器的普通操作系统线程。只有在运行纯Python代码时才会获取GIL(全局解释器锁),而且在许多情况下,它完全被释放甚至不会被检查。
GIL并不会阻止这些操作并行运行。
1. IO操作,例如发送和接收网络数据读写文件。 2. 大量的内置CPU绑定操作,例如哈希计算压缩。 3. 一些C扩展操作,例如numpy计算
任何一个(还有很多其他)都可以以并行方式运行得非常好,在大多数程序中,这些部分都是耗时最长的。
在Python中构建一个示例API,它接收天文数据并计算轨迹,意味着:
- 输入处理和组装网络数据包将会并行进行。 - 如果使用numpy进行轨迹计算,所有计算都将是并行的。 - 将数据添加到数据库将会并行进行。 - 通过网络返回数据也将是并行的。
基本上,全局解释器锁(GIL)不会影响程序运行时间的绝大部分。
此外,至少对于网络编程来说,现在更流行的是其他方法,比如`asyncio`,它在同一线程上提供了协作式多任务处理,有效地消除了线程过载的缺点,并允许同时运行更多的连接。通过利用这一点,GIL甚至不相关。
GIL可能会成为一个问题,并使得在纯Python代码中运行CPU密集型程序时线程无用,比如简单的斐波那契数列计算程序,但在大多数实际情况下,除非你运行一个规模庞大的网站,比如YouTube(诚然,它曾遇到过问题),否则GIL不是一个重要的问题。

1
@redigaffi 在这个回答中,concurrent(并发)和parallel(并行)是等价的。对于之前的混淆,我表示道歉,并已相应地更新了回答。 - Bharel
2
谢谢回复。所以只是为了澄清一下,当使用Python C包装器进行IO操作时,比如file.openfile.writefile.readsocket.sendsocket.recv,Python线程实际上是并行的吗? - redigaffi
1
@redigaffi 是的。 - Bharel
2
@redigaffi asyncio还有进一步的优化,比如使用epoll/IO完成端口,这些都是更有效的单线程调度机制,每个机制都利用了不同的内核/网络驱动能力。许多速度提升都是在操作系统本身内部实现的,与Python无关。 - Bharel
1
@YunWu 更准确地说,Python在运行字节码时从不释放,但有时会在C代码上释放。 - undefined
显示剩余4条评论

3
严格来说,CPython支持多个I/O绑定线程+单个CPU绑定线程。
I/O绑定方法:file.open、file.write、file.read、socket.send、socket.recv等。当Python调用这些I/O函数时,它会隐式地释放GIL,并在I/O函数返回后重新获取GIL。
CPU绑定方法:算术计算等。
C扩展方法:方法必须显式地调用PyEval_SaveThread和PyEval_RestoreThread来告诉Python解释器你正在做什么。

1
请阅读以下内容:https://opensource.com/article/17/4/grok-gil 这里有两个概念:
  1. 协作式多任务处理:当一个线程执行 I/O 绑定的任务时,它会释放 GIL 上的锁,以便其他线程可以继续执行。
  2. 抢占式多任务处理:基本上每个线程都运行一段时间(以执行的字节码数量或时间为单位),然后释放锁,以便其他线程可以继续执行。 因此,虽然一次只有一个线程在运行,但 (1) 意味着我们仍然可以最有效地利用核心 - 请注意,这对于 CPU 绑定的工作负载没有帮助。而 (2) 意味着每个线程都获得了公平分配的 CPU 时间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接