多线程的主要原因之一是程序可以利用多个 CPU(和/或 CPU 上的多个内核)来每秒计算更多操作。但在 Python 中,GIL 意味着即使有多个线程同时进行计算,在任何给定时刻实际上只有其中一个线程会运行,因为所有其他线程都将被阻塞,等待获取全局解释器锁。这意味着 Python 程序的多线程版本实际上是slower比单线程版本更快,因为一次只有一个线程运行——此外,强制每个线程每次都等待、获取然后放弃 GIL(循环方式)会产生记账开销。几毫秒。
为了演示这一点,这里有一个玩具 Python 脚本,它生成指定数量的线程,然后作为其“计算”,每个线程不断增加计数器,直到 5 秒过去。最后,主线程会计算发生的反增量总数并打印总数,以便我们衡量 5 秒期间完成了多少“工作”。
import threading
import sys
import time
numSecondsToRun = 5
class CounterThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self._counter = 0
self._endTime = time.time() + numSecondsToRun
def run(self):
# Simulate a computation on the CPU
while(time.time() < self._endTime):
self._counter += 1
if __name__ == "__main__":
if len(sys.argv) < 2:
print "Usage: python counter 5"
sys.exit(5)
numThreads = int(sys.argv[1])
print "Spawning %i counting threads for %i seconds..." % (numThreads, numSecondsToRun)
threads = []
for i in range(0,numThreads):
t = CounterThread()
t.start()
threads.append(t)
totalCounted = 0
for t in threads:
t.join()
totalCounted += t._counter
print "Total amount counted was %i" % totalCounted
....以下是我在计算机(启用超线程的双核 Mac Mini,FWIW)上得到的结果:
$ python counter.py 1
Spawning 1 counting threads for 5 seconds...
Total amount counted was 14210740
$ python counter.py 2
Spawning 2 counting threads for 5 seconds...
Total amount counted was 10398956
$ python counter.py 3
Spawning 3 counting threads for 5 seconds...
Total amount counted was 10588091
$ python counter.py 4
Spawning 4 counting threads for 5 seconds...
Total amount counted was 11091197
$ python counter.py 5
Spawning 5 counting threads for 5 seconds...
Total amount counted was 11130036
$ python counter.py 6
Spawning 6 counting threads for 5 seconds...
Total amount counted was 10771654
$ python counter.py 7
Spawning 7 counting threads for 5 seconds...
Total amount counted was 10464226
请注意第一次迭代如何实现最佳性能(仅生成一个工作线程);当多个线程同时运行时,计数效率会大幅下降。这显示了 GIL 如何削弱 Python 中的多线程性能——用 C(或任何其他没有 GIL 的语言)编写的相同程序在运行更多线程时会表现出更好的性能,而不是更差(直到工作线程的数量与当然,硬件上的核心数量)。
但这并不意味着多线程在 Python 中完全无用——在大多数或所有线程被阻塞等待 I/O 而不是 CPU 限制的情况下,它仍然有用。这是因为被阻塞等待 I/O 的 Python 线程在等待时不会锁定 GIL,因此在此期间其他线程仍然可以自由执行。但是,如果您需要并行化计算密集型任务(例如光线追踪或计算 Pi 的所有数字或代码破译或类似),那么您将需要使用多个进程而不是多个线程,或者使用不同的语言没有 GIL。