python GIL 的多线程示例

A multi-threading example of the python GIL

我已经阅读了很多关于 "bad" 这个 python GIL 业务在编写多线程代码时的情况,但我从未见过一个例子。有人可以给我一个基本示例,说明在使用线程时 GIL 何时会导致问题。

谢谢!

多线程的主要原因之一是程序可以利用多个 CPUs(and/or 个 CPU 上的多个内核)来计算更多操作每秒。但是在 Python 中,GIL 意味着即使你有多个线程同时进行计算,在任何给定时刻,这些线程中只有一个实际上是 运行,因为所有其他线程都是阻塞,等待获取全局解释器锁。这意味着 Python 程序的多线程版本实际上比单线程版本 ,而不是更快,因为一次只有一个线程运行 -- 加上强制每个线程每隔几毫秒等待、获取然后放弃 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

请注意第一次迭代是如何实现最佳性能的(其中只产生了一个工作线程);当多个线程同时 运行 时,计算效率会大幅下降。这表明 Python 中的多线程性能是如何被 GIL 削弱的——用 C(或任何其他没有 GIL 的语言)编写的相同程序在更多线程 运行 下表现出更好的性能,而不是更差(向上当然,直到工作线程的数量与硬件上的内核数量相匹配。

这并不意味着多线程在 Python 中完全没用——它在大多数或所有线程被阻塞等待 I/O 而不是 [= 的情况下仍然有用33=]-绑定。这是因为阻塞等待 I/O 的 Python 线程在等待时不会锁定 GIL,因此在此期间其他线程仍然可以自由执行。但是,如果您需要并行执行计算密集型任务(例如光线追踪或计算 Pi 的所有数字或密码破解或类似任务),那么您将希望使用多进程而不是多线程,或者使用不同的语言没有 GIL。