Python 无法并行化缓冲区读取

Python fails to parallelize buffer reads

我在多线程中遇到性能问题。

我有一个并行读取 8MB 缓冲区的代码片段:

import copy
import itertools
import threading
import time

# Basic implementation of thread pool.
# Based on multiprocessing.Pool
class ThreadPool:

   def __init__(self, nb_threads):
      self.nb_threads = nb_threads

   def map(self, fun, iter):

      if self.nb_threads <= 1:
         return map(fun, iter)
      nb_threads = min(self.nb_threads, len(iter))

      # ensure 'iter' does not evaluate lazily
      # (generator or xrange...)
      iter = list(iter)

      # map to results list
      results = [None] * nb_threads
      def wrapper(i):
         def f(args):
            results[i] = map(fun, args)
         return f

      # slice iter in chunks
      chunks = [iter[i::nb_threads] for i in range(nb_threads)]

      # create threads
      threads = [threading.Thread(target = wrapper(i), args = [chunk]) \
                 for i, chunk in enumerate(chunks)]

      # start and join threads
      [thread.start() for thread in threads]
      [thread.join() for thread in threads]

      # reorder results
      r = list(itertools.chain.from_iterable(map(None, *results)))
      return r

payload = [0] * (1000 * 1000)  # 8 MB
payloads = [copy.deepcopy(payload) for _ in range(40)]

def process(i):
   for i in payloads[i]:
      j = i + 1

if __name__ == '__main__':

   for nb_threads in [1, 2, 4, 8, 20]:

      t = time.time()
      c = time.clock()

      pool = ThreadPool(nb_threads)
      pool.map(process, xrange(40))

      t = time.time() - t
      c = time.clock() - c

      print nb_threads, t, c

输出:

1 1.04805707932 1.05
2 1.45473504066 2.23
4 2.01357698441 3.98
8 1.56527090073 3.66
20 1.9085559845 4.15

为什么 threading 模块在并行化缓冲区读取时惨遭失败? 是因为 GIL 吗?或者因为我的机器上有一些奇怪的配置,一个进程 一次只允许对 RAM 进行一次访问(如果我将 ThreadPool 切换为 multiprocessing.Pool ,我会得到不错的加速是上面的代码)?

我在 linux 发行版上使用 CPython 2.7.8。

是的,Python 的 GIL 阻止 Python 代码在 运行 中跨多个线程并行执行。您将您的代码描述为 "buffer reads",但它实际上是 运行 任意 Python 代码(在本例中,遍历一个列表,将 1 加到其他整数上)。如果您的线程正在进行阻塞系统调用(例如从文件或网络套接字读取),那么 GIL 通常会在线程阻塞等待外部数据时被释放。但是由于对 Python 对象的大多数操作都会产生副作用,因此您不能同时执行其中的几个操作。

一个重要的原因是 CPython 的垃圾收集器使用引用计数作为其了解何时可以清理对象的主要方式。如果多个线程试图同时更新同一个对象的引用计数,它们可能会以竞争状态结束并使对象的引用计数错误。 GIL 可以防止这种情况发生,因为一次只能有一个线程进行此类内部更改。每次您的 process 代码执行 j = i + 1 时,它都会更新整数对象 01 的引用计数各几次。这正是 GIL 存在的目的。