首页 > 解决方案 > Python 无法并行化缓冲区读取

问题描述

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

我有一个并行读取 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

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

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

标签: multithreadingpython-2.7gil

解决方案


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

造成这种情况的一个重要原因是 CPython 的垃圾收集器使用引用计数作为了解何时可以清理对象的主要方式。如果多个线程尝试同时更新同一个对象的引用计数,它们可能会最终陷入竞争状态,并使对象的计数错误。GIL 可以防止这种情况发生,因为一次只能有一个线程进行此类内部更改。每次您的process代码执行此操作j = i + 1时,它都会更新整数对象的引用计数,0并且每次都会更新1几次。这正是 GIL 所要保护的东西。


推荐阅读