如何从并行进程中运行的函数中检索值?

时间:2018-11-13 19:29:53

标签: python python-3.x parallel-processing multiprocessing python-multiprocessing

对于python初学者来说,Multiprocessing模块非常令人困惑,特别是对于刚从MATLAB迁移并因其并行计算工具箱而变得懒惰的人。我有以下功能需要约80秒的运行时间,我想通过使用Python的多处理模块来缩短此时间。

from time import time

xmax   = 100000000

start = time()
for x in range(xmax):
    y = ((x+5)**2+x-40)
    if y <= 0xf+1:
        print('Condition met at: ', y, x)
end  = time()
tt   = end-start #total time
print('Each iteration took: ', tt/xmax)
print('Total time:          ', tt)

这将按预期输出:

Condition met at:  -15 0
Condition met at:  -3 1
Condition met at:  11 2
Each iteration took:  8.667453265190124e-07
Total time:           86.67453265190125

由于循环的任何迭代都不依赖于其他迭代,因此我尝试采用官方文档中的Server Process来在单独的进程中扫描范围的块。最后,我想到了vartec对this question的回答,可以准备以下代码。我还根据Darkonaut对当前问题的回答更新了代码。

from time import time 
import multiprocessing as mp

def chunker (rng, t): # this functions makes t chunks out of rng
    L  = rng[1] - rng[0]
    Lr = L % t
    Lm = L // t
    h  = rng[0]-1
    chunks = []
    for i in range(0, t):
        c  = [h+1, h + Lm]
        h += Lm
        chunks.append(c)
    chunks[t-1][1] += Lr + 1
    return chunks

def worker(lock, xrange, return_dict):
    '''worker function'''
    for x in range(xrange[0], xrange[1]):
        y = ((x+5)**2+x-40)
        if y <= 0xf+1:
            print('Condition met at: ', y, x)
            return_dict['x'].append(x)
            return_dict['y'].append(y)
            with lock:                
                list_x = return_dict['x']
                list_y = return_dict['y']
                list_x.append(x)
                list_y.append(y)
                return_dict['x'] = list_x
                return_dict['y'] = list_y

if __name__ == '__main__':
    start = time()
    manager = mp.Manager()
    return_dict = manager.dict()
    lock = manager.Lock()
    return_dict['x']=manager.list()
    return_dict['y']=manager.list()
    xmax = 100000000
    nw = mp.cpu_count()
    workers = list(range(0, nw))
    chunks = chunker([0, xmax], nw)
    jobs = []
    for i in workers:
        p = mp.Process(target=worker, args=(lock, chunks[i],return_dict))
        jobs.append(p)
        p.start()

    for proc in jobs:
        proc.join()
    end = time()
    tt   = end-start #total time
    print('Each iteration took: ', tt/xmax)
    print('Total time:          ', tt)
    print(return_dict['x'])
    print(return_dict['y'])

可将运行时间大幅减少至〜17秒。但是,我的共享变量无法检索任何值。请帮助我找出代码的哪一部分出了错。

我得到的输出是:

Each iteration took:  1.7742713451385497e-07
Total time:           17.742713451385498
[]
[]
我期望的

Each iteration took:  1.7742713451385497e-07
Total time:           17.742713451385498
[0, 1, 2]
[-15, -3, 11]

1 个答案:

答案 0 :(得分:3)

您的示例中的问题在于,不会传播对Manager.dict中的标准可变结构的修改。我首先向您展示如何使用Manager进行修复,然后再向您展示更好的选择。

multiprocessing.Manager有点沉重,因为它仅为Manager使用了一个单独的Process,并且在共享对象上工作需要使用锁来确保数据的一致性。如果您在一台计算机上运行此程序,则multiprocessing.Pool有更好的选择,以防您不必运行自定义的Process类,并且如果必须运行multiprocessing.Process和{{ 1}}是这样做的常见方法。

引号部分来自多重处理docs.


经理

  

如果引用中包含标准(非代理)列表或dict对象,则对这些可变值的修改将不会通过管理器传播,因为代理无法知道何时修改了其中包含的值。但是,将值存储在容器代理中(触发代理对象上的 setitem )确实会通过管理器传播,因此要有效地修改此类项目,可以将修改后的值重新分配给容器代理...

在您的情况下,它看起来像:

multiprocessing.Queue

这里的def worker(xrange, return_dict, lock): """worker function""" for x in range(xrange[0], xrange[1]): y = ((x+5)**2+x-40) if y <= 0xf+1: print('Condition met at: ', y, x) with lock: list_x = return_dict['x'] list_y = return_dict['y'] list_x.append(x) list_y.append(y) return_dict['x'] = list_x return_dict['y'] = list_y 将是一个lock实例,您必须将其作为参数传递,因为整个(现在)锁定的操作本身并不是原子的。 (Here manager.Lock使用Lock是一个更简单的示例)

  

在大多数情况下,这种方法可能不如使用嵌套的代理对象方便,但它也展示了对同步的控制级别。

由于Python 3.6代理对象是可嵌套的:

  

在3.6版中进行了更改:共享对象能够嵌套。例如,共享容器对象(例如共享列表)可以包含其他共享对象,这些对象都将由SyncManager管理和同步。

从Python 3.6开始,您可以在以Manager作为值开始多处理之前填充manager.dict,然后直接将其追加到工作线程中,而无需重新分配。

manager.list

编辑:

这是return_dict['x'] = manager.list() return_dict['y'] = manager.list() 的完整示例:

Manager

游泳池

import time import multiprocessing as mp from multiprocessing import Manager, Process from contextlib import contextmanager # mp_util.py from first link in code-snippet for "Pool" # section below from mp_utils import calc_batch_sizes, build_batch_ranges # def context_timer ... see code snippet in "Pool" section below def worker(batch_range, return_dict, lock): """worker function""" for x in batch_range: y = ((x+5)**2+x-40) if y <= 0xf+1: print('Condition met at: ', y, x) with lock: return_dict['x'].append(x) return_dict['y'].append(y) if __name__ == '__main__': N_WORKERS = mp.cpu_count() X_MAX = 100000000 batch_sizes = calc_batch_sizes(X_MAX, n_workers=N_WORKERS) batch_ranges = build_batch_ranges(batch_sizes) print(batch_ranges) with Manager() as manager: lock = manager.Lock() return_dict = manager.dict() return_dict['x'] = manager.list() return_dict['y'] = manager.list() tasks = [(batch_range, return_dict, lock) for batch_range in batch_ranges] with context_timer(): pool = [Process(target=worker, args=args) for args in tasks] for p in pool: p.start() for p in pool: p.join() # Create standard container with data from manager before exiting # the manager. result = {k: list(v) for k, v in return_dict.items()} print(result) 通常会这样做。在示例中,您还面临其他挑战,因为您希望在一个范围内分布迭代。 即使您的multiprocessing.Pool函数也无法划分范围,所以每个进程的工作大致相同:

chunker

对于下面的代码,请从我的答案here中获取chunker((0, 21), 4) # Out: [[0, 4], [5, 9], [10, 14], [15, 21]] # 4, 4, 4, 6! 的代码片段,它提供了两个功能,可对块范围进行尽可能的控制。

使用mp_utils.py,您的multiprocessing.Pool函数只需要返回结果,而worker将负责将结果通过内部队列传输回父进程。 Pool将是一个列表,因此您将不得不以您希望的方式重新排列结果。您的示例如下所示:

result

示例输出:

import time
import multiprocessing as mp
from multiprocessing import Pool
from contextlib import contextmanager
from itertools import chain

from mp_utils import calc_batch_sizes, build_batch_ranges

@contextmanager
def context_timer():
    start_time = time.perf_counter()
    yield
    end_time = time.perf_counter()
    total_time   = end_time-start_time
    print(f'\nEach iteration took: {total_time / X_MAX:.4f} s')
    print(f'Total time:          {total_time:.4f} s\n')


def worker(batch_range):
    """worker function"""
    result = []
    for x in batch_range:
        y = ((x+5)**2+x-40)
        if y <= 0xf+1:
            print('Condition met at: ', y, x)
            result.append((x, y))
    return result


if __name__ == '__main__':

    N_WORKERS = mp.cpu_count()
    X_MAX = 100000000

    batch_sizes = calc_batch_sizes(X_MAX, n_workers=N_WORKERS)
    batch_ranges = build_batch_ranges(batch_sizes)
    print(batch_ranges)

    with context_timer():
        with Pool(N_WORKERS) as pool:
            results = pool.map(worker, iterable=batch_ranges)

    print(f'results: {results}')
    x, y = zip(*chain.from_iterable(results))  # filter and sort results
    print(f'results sorted: x: {x}, y: {y}')

如果您的[range(0, 12500000), range(12500000, 25000000), range(25000000, 37500000), range(37500000, 50000000), range(50000000, 62500000), range(62500000, 75000000), range(75000000, 87500000), range(87500000, 100000000)] Condition met at: -15 0 Condition met at: -3 1 Condition met at: 11 2 Each iteration took: 0.0000 s Total time: 8.2408 s results: [[(0, -15), (1, -3), (2, 11)], [], [], [], [], [], [], []] results sorted: x: (0, 1, 2), y: (-15, -3, 11) Process finished with exit code 0 有多个参数,则可以使用参数元组构建一个“任务”列表,并与worker交换pool.map(...)。有关更多详细信息,请参阅文档。


处理和排队

如果由于某种原因无法使用pool.starmap(...iterable=tasks),则必须采取 通过以下方式自己照顾进程间通信(IPC): multiprocessing.Pool作为子级中您的工作者功能的参数, 流程,让他们排队将其结果发送回 父母

您还必须构建类似Pool的结构,以便对其进行迭代以启动和加入流程,并且必须从队列中multiprocessing.Queue返回结果。我已经写过here,有关get()使用的更多信息。

采用这种方法的解决方案如下所示:

Queue.get