why is more than one worker used in `multiprocessing.Pool().apply_async()`?

被刻印的时光 ゝ 提交于 2019-12-05 23:11:17

Your confusion seems to come from thinking [pool.apply_async(...) for i in range(10)] is one call, when there are really ten independent calls. A call to any pool-method is a "job". A job generally can lead to one or multiple tasks being distributed. apply-methods always produce only a single task under the hood. A task is an indivisible unit of work which will be received as a whole by a random pool-worker.

There's only one shared inqueue, all workers are fed over. Which idling worker will be woken up from waiting to get() a task from that queue is up to the OS. Your result-entropy for case 1 is still somewhat surprising and probably very lucky, at least unless you confirm you have only two cores.

And yes, your observation for this run is also influenced by the computation time needed for a task, since threads (the scheduled execution unit within a process) usually are scheduled with time slicing policies (e.g. ~20ms for Windows).

Only one worker is used for that call. A single apply_async cannot be executed in two workers. That doesn't prevent multiple apply_async calls from being executed in different workers. Such a restriction would run completely counter to the point of having a process pool at all.

Spurred by @Darkonaut's comment, I inspected further and observed the blocking function was too fast. I tested the latter code, modified, with a new intensive blocking function.

Code

The new blocking function iteratively computes Fibonacci numbers. An optional argument can be passed in to broaden the range and compute larger numbers.

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")

Demo

Passing in a large integer (100000) to the offset parameter, e.g. ...[pool.apply_async(blocking_func, args=(i, 100000)) ...] and running the code, we are more reliably able to trigger process switching.

# 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

It's intriguing to note that 100k Fibonacci numbers are asynchronously being computed 10 times in less than 2 seconds. By contrast, using a recursive implementation of Fibonacci would be comparably intensive at ~30 iterations (not shown).

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!