为什么在multiprocessing.Pool()。apply_async()中使用了多个工人?

时间:2019-02-19 00:48:52

标签: python multiprocessing python-multiprocessing

问题

来自multiprocessing.Pool docs

  

apply_async(func ...) apply()方法的一种变体,它返回结果对象。 ...

进一步阅读...

  

apply(func[, args[, kwds]]):使用参数args和关键字参数kwds调用func。它会阻塞直到结果准备就绪。给定此块,apply_async()更适合于并行执行工作。 此外,func仅在池中的一个工作程序中执行。

最后一个粗线建议仅使用池中的一个工人。我发现这仅在某些条件下是正确的。

给出

这是在三种类似情况下执行Pool.apply_async()的代码。在所有情况下,都会打印进程ID。

import os
import time
import multiprocessing as mp


def blocking_func(x, delay=0.1):
    """Return a squared argument after some delay."""
    time.sleep(delay)                                  # toggle comment here
    return x*x, os.getpid()


def apply_async():
    """Return a list applying func to one process with a callback."""
    pool = mp.Pool()
    # Case 1: From the docs
    results = [pool.apply_async(os.getpid, ()) for _ in range(10)]
    results = [res.get(timeout=1) for res in results]
    print("Docs    :", results)

    # Case 2: With delay
    results = [pool.apply_async(blocking_func, args=(i,)) for i in range(10)]
    results = [res.get(timeout=1)[1] for res in results]
    print("Delay   :", results)

    # Case 3: Without delay
    results = [pool.apply_async(blocking_func, args=(i, 0)) for i in range(10)]
    results = [res.get(timeout=1)[1] for res in results]
    print("No delay:", results)

    pool.close()
    pool.join()


if __name__ == '__main__':
    apply_async()

结果

docs(案例1)中的示例确认仅运行了一个工作程序。在接下来的情况下,我们通过应用blocking_func扩展此示例,该操作会稍有延迟。

注释time.sleep()中的blocking_func()行会使所有情况都达成一致。

# Time commented
# 1. Docs    : [8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208]
# 2. Delay   : [8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208]
# 3. No delay: [8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208, 8208]

每次调用apply_async()都会创建一个新的进程池,这就是为什么新的进程ID与后者不同的原因。

# Time uncommented
# 1. Docs    : [6780, 6780, 6780, 6780, 6780, 6780, 6780, 6780, 6780, 6780]
# 2. Delay   : [6780, 2112, 6780, 2112, 6780, 2112, 6780, 2112, 6780, 2112]
# 3. No delay: [6780, 2112, 6780, 2112, 6780, 2112, 6780, 2112, 6780, 2112]

但是,即使没有延迟time.sleep()的注释,即使延迟为零,也会使用多个工作线程。

简而言之,我们没有期望像案例1那样有一名工人,但是就像案例2和3一样,我们会有多名工人。

问题

尽管我期望Pool().apply_async()仅使用一个工人,但是如果不注释time.sleep(),为什么要使用一个以上的工人?封锁是否应该影响applyapply_async使用的工作人员数量?

注意:以前的相关问题问“为什么只雇用一名工人?”这个问题的问题恰恰相反:“为什么不是只使用一个工人?”我在Windows计算机上使用2个内核。

3 个答案:

答案 0 :(得分:2)

您的困惑似乎来自于认为[pool.apply_async(...) for i in range(10)] 一个 电话,而实际上有个独立电话。调用任何池方法都是一种“工作”。一项工作通常可以导致分配一个或多个任务。 apply方法始终只能在后台执行单个任务。任务是一个不可分割的工作单元,将由一个随机的池工作人员整体接收。

只有一个共享的inqueue,所有工作人员都受够了。哪个空闲工作者将从等待到get()的队列中醒来,取决于操作系统。情况1的结果熵仍然有些令人惊讶,并且可能非常幸运,至少除非您确认只有两个核心。

是的,您对运行的观察也受任务所需的计算时间的影响,因为线程(进程中的预定执行单元)通常使用时间切片策略进行调度(例如,对于Windows为约20ms)。 / p>

答案 1 :(得分:1)

该呼叫仅使用一名工人。单个apply_async不能在两个工作线程中执行。这不会阻止在不同的工作程序中执行多个apply_async调用。这样的限制将完全与拥有进程池完全相反。

答案 2 :(得分:0)

在@Darkonaut的评论的刺激下,我进一步检查了一下,发现阻塞功能太快了。我使用新的密集阻止功能测试了修改后的代码。

代码

新的阻塞函数迭代地计算斐波那契数。可以传入一个可选参数以扩大范围并计算更大的数字。

def blocking_func(n, offset=0):
    """Return an intensive result via Fibonacci number."""
    n += offset
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a, os.getpid()


def blocking_func(n, offset=0):
    """Return an intensive result via recursive fibonacci number."""
    func = blocking_func
    n += offset
    if n <= 1:
        return n, os.getpid()
    return func(n-1) + func(n-2)

if __name__ == '__main__':        
    start = time.time()
    apply_async()
    end = time.time()
    print(f"Duration : {end - start:.2f}s")

演示

将大整数(100000)传递给offset参数,例如...[pool.apply_async(blocking_func, args=(i, 100000)) ...]并运行代码,我们可以更可靠地触发流程切换。

# Results
Docs     : [10032, 10032, 10032, 10032, 10032, 10032, 10032, 10032, 10032, 10032]
Offset   : [10032, 8268, 10032, 8268, 10032, 8268, 10032, 8268, 10032, 8268]
Duration : 1.67s

有趣的是,在不到2秒的时间内,异步计算了10万斐波纳契数10次。相比之下,使用Fibonacci的递归实现在大约30次迭代(未显示)时会比较密集。