在Python3.11之前,Python的多线程都是伪多线程。以前只知道这一点,但是从来没有验证过,也没有真正理解它的意思。今天刷知乎看到了一篇关于解释Python伪多线程的文章,同时Python3.12也早就公布要支持真正的多线程,这篇文章就详细探索一下Python的多线程。
知乎原文链接:为什么有人说 Python 的多线程是鸡肋呢?
本文中内容为个人测试,仅代表个人观点,可能与事实不符,请知悉。
Python代码是如何执行的
在明确Python的线程之前,有一个重要的概念叫做全局解释器锁(GIL)。Python代码是由解释器执行的,它在设计之初就考虑在主循环中同时只有一个线程在执行,就像单CPu的系统中运行多个进程那样。随着Python的发展,它也逐步完善了多线程,但是至始至终这几个现场也是逐个排队,分步完成的,而不是真正地并行执行。
在多线程中,每个线程访问解释器的顺序就由全局解释器锁控制。该锁用于保证每次只要一个线程在运行。在多线程环境中,Python虚拟机按照以下方式执行:
- 设置GIL;
- 切换到一个线程执行;
- 运行;
- 将线程设置为休眠状态;
- 解锁GIL;
- 再次重复以上步骤。
对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。也就是说,I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。
我们都知道,比方我有一个4核的CPU,那么这样一来,在单位时间内每个核只能跑一个线程,然后时间片轮转切换。但是Python不一样,它不管你有几个核,单位时间多个核只能跑一个线程,然后时间片轮转。看起来很不可思议?但是这就是GIL搞的鬼。任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
让我们通过一个具体的例子看一下Python3.11的多线程。
python 代码:from threading import Thread
def run():
while True:
...
threads = [Thread(target=run) for _ in range(15)]
for thread in threads:
thread.start()
while True:
...
此代码中有三个子线程外加一个主线程,共四个线程,运行此程序后就在任务管理器中查看线程运行情况。

实际上运行前后任务管理器中线程变化数并不明显。
虽然如此,Python的多进程是可以充分利用CPU的:
python 代码:from multiprocessing import Process
def run():
while True:
...
if __name__ == '__main__':
processes = [Process(target=run) for _ in range(15)]
for process in processes:
process.start()
while True:
...

可以看到Python的多进程是可以充分利用CPU的。
接下来我写一段具有同样意义的Java代码,看看Java的多线程是否可以占满CPU。
java 代码:public class Main {
public static void main(String[] args) {
for (int i = 0; i < 15; i++) {
new Thread(() -> {
while (true) {
}
}).start();
}
}
}

运行了30秒,CPU全部100%。我想这下应该足够证明Python的多线程是伪多线程了。
Python 的多线程之所以被称为“伪多线程”,是因为 Python 在 CPython 解释器中使用了全局解释器锁(Global Interpreter Lock,GIL)。GIL 是一把互斥锁,它确保了同一时间只有一个线程在执行 Python 字节码。即使在多核 CPU 上,由于 GIL 的存在,Python 的多线程程序也只能在单个 CPU 核心上运行。
这样的设计导致了在 CPU 密集型任务中,Python 的多线程并不能真正实现并行计算,因为任何时候都只有一个线程在执行。然而,在 I/O 密集型任务中,例如网络请求或文件操作,Python 的多线程仍然可以发挥一定的作用,因为线程在等待 I/O 完成时会释放 GIL,其他线程可以继续执行。
Python3.12对于多线程的支持
Python3.12引入了“Per-Interpreter GIL”,这使得各个Python解释器不再共享同一个GIL。这种隔离级别允许每个子解释器可以同时运行。这意味着我们可以通过生成额外的子解释器来绕过Python的并发限制,其中每个子解释器都有自己的GIL(全局状态)。通过从每个进程一个全局解释器锁过渡到每个子解释器一个全局解释器锁,Python正在采取措施改善多线程并行性。通过PEP 684和PEP 554,可以从Python中创建子解释器,从而实现真正的多线程并行。
但是这种方法需要按照文档创造子解释器,实际操作较为麻烦。
还是使用相同的代码简单测试一下,看看线程占用如何吧:
python 代码:from multiprocessing import Process
def run():
while True:
...
if __name__ == '__main__':
processes = [Process(target=run) for _ in range(200)]
for process in processes:
process.start()
while True:
pass

实际上使用前后线程有很大的改变。
也不知道是不是Python3.12做的优化导致了此结果,有大神的话可以在评论区指正。
通过优化Python多线程,Python中很多地方就可以实现真正地并行运行了, 还有一种实现多线程的方法是开启子解释器,或者使用C语言作为线程运行的基础,这里就不演示了。
有一说一,Python效率还是可以的~
暂无点赞
暂无点赞