Python多线程与GIL全局解释器锁的真相解析:为何被称为假多线程?

不过,最近有读者问:

Python多线程与GIL全局解释器锁的真相解析:为何被称为假多线程?

Python的多线程是真是假?

突然我点击了Python的一个让人百感交集的特性——GIL。

这是怎么回事?今天我们来谈谈吧。

完美的

我们知道Python灵活而强大,因为它是一种解释型语言,可以同时解释和执行。实现此功能的标准实现称为 CPython。

它分两步运行Python程序:

灵活性是可以的,但是为了保证程序执行的稳定性,付出了巨大的代价:

引入全局解释器锁GIL(全局解释器锁)[2]

这样可以保证同一时间只有一个字节码在运行,这样就不会因为没有事先编译而导致资源争用和状态混乱的问题。

看似“完美”,但这样做意味着多线程执行时会被GIL变成单线程,硬件资源无法充分利用。

我们看一下代码:

import time
def gcd(pair):
    '''
    求解最大公约数
    '''
    a, b = pair
    low = min(a, b)
    for i in range(low, 0, -1):
        if a % i == 0 and b % i == 0:
            return i
    
    assert False, "Not reachable"
# 待求解的数据
NUMBERS = [
    (1963309, 2265973), (5948475, 2734765),
    (1876435, 4765849), (7654637, 3458496),
    (1823712, 1924928), (2387454, 5873948),
    (1239876, 2987473), (3487248, 2098437),
    (1963309, 2265973), (5948475, 2734765),
    (1876435, 4765849), (7654637, 3458496),
    (1823712, 1924928), (2387454, 5873948),
    (1239876, 2987473), (3487248, 2098437),
    (3498747, 4563758), (1298737, 2129874)
]
## 顺序求解
start = time.time()
results = list(map(gcd, NUMBERS))
end = time.time()
delta = end - start
print(f'顺序执行时间: {delta:.3f} 秒')

在笔者的电脑(4核,16G)上执行时间为2.043秒。

如何切换到多线程?

...
from concurrent.futures import ThreadPoolExecutor
...
## 多线程求解
start = time.time()
pool = ThreadPoolExecutor(max_workers=4)
results = list(pool.map(gcd, NUMBERS))
end = time.time()
delta = end - start
print(f'执行时间: {delta:.3f} 秒')

看一下运行效果:

顺序执行时间: 2.045 秒
并发执行时间: 2.070 秒

什么?

并行执行需要更长的时间!

如果连续执行多次,结果将是相同的,这意味着在GIL的限制下,多线程是无效的,并且由于线程调度而损失了更多的时间。

带着枷锁跳舞

Python中的多线程真的没用吗?

事实上,事实并非如此。虽然因为GIL而无法实现真正​​意义上的多线程,但是多线程机制仍然为我们提供了两个重要的特性。

1:多线程编写可以让某些程序更容易编写

怎么理解呢?

如果要解决一个需要同时维护多个状态的程序,用单线程来实现是非常困难的。

例如,如果要检索文本文件中的数据,为了提高检索效率,可以将文件分成小段进行处理。如果首先在该段中找到该文件,则处理将结束。

使用单个线程,很难同时考虑多个段。检索任务只能按顺序执行或使用二分法执行。

通过多线程,每个段都可以交给每个线程,依次执行,相当于同时推荐检索任务。处理时,相比顺序查找,效率会大大提高。

2:更高效地处理阻塞I/O任务

阻塞I/O是指当系统需要与文件系统交互时(包括网络和终端显示),由于文件系统处理速度比CPU慢很多,程序会被设置为阻塞状态,即,不不再分配计算资源。

直到文件系统的结果返回后才会被激活,有机会再次分配计算资源。

换句话说,处于阻塞状态的程序将永远等待。

所以如果一个程序需要不断地从文件系统读取数据,处理完后再写入,如果是单线程的话,需要等待读取再处理,等待处理完成再写入,这样处理过程就变成了逐个。的等待。

有了多线程,当一个处理进程被阻塞时,会立即被GIL切掉,将计算资源分配给其他可执行进程,从而提高执行效率。

有了这两个特性,就说明Python的多线程并不是一无是处。如果能根据情况来写,效率会大大提高。然而,对于计算密集型任务,多线程功能却无济于事。

曲线救国

那么有没有一种方法可以真正利用计算资源而不受GIL的束缚呢?

当然有,而且不止一个。

我们先介绍一个简单易用的方法。

回顾之前计算最大公约数的程序,我们使用了线程池来处理,但是并没有什么效果,比不使用还糟糕。

这是因为这个程序是计算密集型的,严重依赖CPU,并且明显受GIL限制。

现在我们稍微修改一下程序:

...
from concurrent.futures import ProcessPoolExecutor
...
## 并行程求解
start = time.time()
pool = ProcessPoolExecutor(max_workers=4)
results = list(pool.map(gcd, NUMBERS))
end = time.time()
delta = end - start
print(f'并行执行时间: {delta:.3f} 秒')

看看效果:

顺序执行时间: 2.018 秒
并发执行时间: 2.032 秒
并行执行时间: 0.789 秒

并行执行提升了近3倍!这是怎么回事?

仔细看的话可以发现,主要就是把多线程中的ThreadPoolExecutor替换成了ProcessPoolExecutor,也就是进程池执行器。

同一进程中的Python程序会受到GIL的限制,但不同进程中则不会,因为每个进程中的GIL是独立的。

是不是很神奇呢?在这里,感谢concurrent.futures模块,封装了实现进程池的复杂性,给我们留下了一个简单而优雅的界面。

这里需要注意的是,ProcessPoolExecutor并不是万能的。更适合数据相关性不高、计算密集的场景。

如果数据相关性很强,进程之间就会发生“通信”,这可能会抵消来之不易的性能提升。

还有哪些方法可以处理进程池呢?那是:

用C语言重写需要提高性能的部分

不要感到惊讶,Python 已经有了用于 C 扩展的 API。

但这样做需要付出更多的成本。为此,您还可以使用 SWIG[3] 和 CLIF[4] 等工具将 python 代码转换为 C 代码。

有兴趣的读者可以研究一下。

不断自我完善

了解了Python多线程的问题和解决方案后,对于我们这些热爱Python的人来说,我们该何去何从呢?

有一句话用在这里非常合适:

求自己不如求别人

无论工具或武器多么强大,都不能解决所有问题,而问题之所以能够解决,主要是因为我们的主观能动性。

这不就是我们需要分析判断情况,选择合适的解决方案吗?

对于Python中多线程的批评,我们大多看到它阳光明媚的一面,在需要提高速度的地方采取适当的方法。以下是一个简短的总结:

对于I/O密集型任务,使用Python的多线程完全没有用处,并且可以大大提高执行效率。对于计算密集型任务,就看数据依赖性是否低。如果是的话,使用ProcessPoolExecutor代替多线程完全可以。如果使用硬件资源时数据依赖性较高,可以考虑使用C来实现关键区域。一方面,C 本身比 Python 更快。另一方面,C 可以使用较低级别的多线程机制,而完全不用担心。受制于GIL 大多数情况下,对于只能通过多线程处理的任务,不需要考虑太多。只需利用Python的多线程机制即可。你不需要想太多。概括

没有完美的解决方案。即使有,也只能是在某些特定条件下。就像软件工程中没有灵丹妙药一样。

面对现实的世界,我们能依靠的只有我们自己。我们通过学习了解更多,通过实践感受更多,通过总结和回顾收获更多,通过思考和反思解决更多。这是我们人类不断发展的动力。

你可能想看:
« 上一篇
下一篇 »

相关推荐

python爬虫教程PDF,Python爬虫书PDF

2026年09月02日

31阅读

外国教程python,python编程教学视频

2026年09月02日

25阅读

python简明教程沈洁元,python详细基础教程

2026年09月02日

21阅读

python库包教程,python 包

2026年09月02日

26阅读

python教程大全集,python教程600集

2026年09月02日

26阅读

python入门教程完整版,python零基础入门全套教程视频

2026年09月02日

22阅读