multiprocessing.Pool 对比 multiprocessing.pool.ThreadPool

multiprocessing.Pool vs multiprocessing.pool.ThreadPool

下面是multiprocessing.Pool vs multiprocessing.pool.ThreadPool vs sequential version的一些测试,我想知道为什么multiprocessing.pool.ThreadPool版本比sequential version慢?

multiprocessing.Pool 确实更快,因为它使用进程(即没有 GIL)并且 multiprocessing.pool.ThreadPool 使用线程(即有 GIL)尽管包的名称为 multiprocessing

import time


def test_1(job_list):
    from multiprocessing import Pool

    print('-' * 60)
    print("Pool map")
    start = time.time()
    p = Pool(8)
    s = sum(p.map(sum, job_list))
    print('time:', time.time() - start)


def test_2(job_list):
    print('-' * 60)
    print("Sequential map")
    start = time.time()
    s = sum(map(sum, job_list))
    print('time:', time.time() - start)


def test_3(job_list):
    from multiprocessing.pool import ThreadPool

    print('-' * 60)
    print("ThreadPool map")
    start = time.time()
    p = ThreadPool(8)
    s = sum(p.map(sum, job_list))
    print('time:', time.time() - start)


if __name__ == '__main__':
    job_list = [range(10000000)]*128

    test_1(job_list)

    test_2(job_list)

    test_3(job_list)

输出:

------------------------------------------------------------
Pool map
time: 3.4112906455993652
------------------------------------------------------------
Sequential map
time: 23.626681804656982
------------------------------------------------------------
ThreadPool map
time: 76.83279991149902

您的任务纯粹是 CPU 绑定(在 I/O 上没有阻塞)并且没有使用任何手动释放 GIL 的扩展代码来执行大量 number-c运行 ching 不涉及 Python 级引用计数对象(例如 hashlib 哈希大输入、大数组 numpy 计算等)。因此,GIL 的定义阻止您从代码中提取 any 并行性;只有一个线程可以同时持有 GIL 并执行 Python 字节码,你会变慢,因为:

  1. 您必须启动所有这些线程
  2. 他们必须在他们之间交出 GIL 来模拟并行处理
  3. 您必须清理所有线程

简而言之,是的,ThreadPool does what it says on the tin:它提供与 Pool 相同的 API,但由线程支持,而不是工作进程,因此没有避免 GIL 限制和开销.直到最近它才被直接记录下来;相反,它是由 multiprocessing.dummy 文档间接记录的,这些文档更明确地提供了 multiprocessing API 但由线程支持,而不是进程(您将其用作 multiprocessing.dummy.Pool,没有名称实际上包括单词“Thread”)。

我会注意到您的测试使 Pool 看起来比正常情况下更好。通常,Pool 对这样的任务(大量数据,相对于数据大小的计算很少)会表现不佳,因为序列化数据并将其发送到子进程的成本超过了并行化工作的微小收益.但是由于你的“大数据”是由 range 对象表示的(它们被廉价地序列化,作为对 range class 的引用和重建它的参数),很少的数据是转移给工人和从工人那里转移。如果您使用真实数据(实现了 intlist),Pool 带来的好处会急剧下降。例如,只需将 job_list 的定义更改为:

job_list = [[*range(10000000)]] * 128

在我的机器上 Pool 的时间(对于未修改的 Pool 情况需要 3.11 秒)跳到 8.11 秒。甚至这是一个谎言,因为 pickle 序列化代码识别一遍又一遍重复的相同 list 并只序列化内部 list 一次,然后快速重复它“先看那个 list" 代码。我会告诉你使用什么:

job_list = [[*range(10000000)] for _ in range(128)]

做了 运行 时间,但我只是试图 运行 那行差点让我的机器崩溃(它需要 ~46 GB 的内存来创建 listlists,并且该成本将在父进程中支付一次,然后在子进程中再次支付);可以这么说,Pool 会丢失非常严重,尤其是在数据只适合 RAM 一次而不适合两次的情况下。