为什么在python map()和multiprocessing.Pool.map()得到了不同的答案?

时间:2011-08-13 01:20:59

标签: python map multiprocessing

我有一个奇怪的问题。我有一个格式为的文件:

START
1
2
STOP
lllllllll
START
3
5
6
STOP

我希望将STARTSTOP之间的行视为块,并使用my_f来处理每个块。

def block_generator(file):

with open(file) as lines:
    for line in lines:
        if line == 'START': 
            block=itertools.takewhile(lambda x:x!='STOP',lines) 
            yield block   

在我的主要功能中,我尝试使用map()来完成工作。它奏效了。

blocks=block_generator(file)
map(my_f,blocks)

实际上会给我我想要的东西。但是当我用multiprocessing.Pool.map()尝试同样的事情时,它给了我一个错误,说takewhile()想要取2个参数,给出0。

    blocks=block_generator(file)
    p=multiprocessing.Pool(4) 
    p.map(my_f,blocks)

这是一个错误吗?

  1. 该文件有超过1000000个块,每个块少于100行。
  2. 我接受了unubu的答案。
  3. 但也许我会简单地拆分文件并使用原始脚本的n实例而不进行多处理来处理它们然后将结果合并在一起。这样,只要脚本适用于小文件,就永远不会出错。

2 个答案:

答案 0 :(得分:2)

怎么样:

import itertools

def grouper(n, iterable, fillvalue=None):
    # Source: http://docs.python.org/library/itertools.html#recipes
    "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    return itertools.izip_longest(*[iter(iterable)]*n,fillvalue=fillvalue)

def block_generator(file):
    with open(file) as lines:
        for line in lines:
            if line == 'START': 
                block=list(itertools.takewhile(lambda x:x!='STOP',lines))
                yield block

blocks=block_generator(file)
p=multiprocessing.Pool(4)
for chunk in grouper(100,blocks,fillvalue=''):
    p.map(my_f,chunk)

使用grouper将限制p.map消耗的文件数量。因此,不需要立即将整个文件读入内存(送入任务队列)。


我在上面声称,当你调用p.map(func,iterator)时,整个迭代器会被立即消耗以填充任务队列。然后,池工作者从队列中获取任务并同时处理作业。

如果您查看pool.py内部并查看定义,您会看到 _handle_tasks线程从self._taskqueue获取项目,并立即枚举:

         for i, task in enumerate(taskseq):
             ...
             put(task)

结论是,传递给p.map的迭代器立刻被消耗掉了。在从队列中获取下一个任务之前,没有等待一个任务结束。

作为进一步的佐证,如果你运行这个:

演示代码:

import multiprocessing as mp
import time
import logging

def foo(x):
    time.sleep(1)
    return x*x

def blocks():
    for x in range(1000):
        if x%100==0:
            logger.info('Got here')
        yield x

logger=mp.log_to_stderr(logging.DEBUG)
logger.setLevel(logging.DEBUG) 
pool=mp.Pool() 
print pool.map(foo, blocks()) 

您会看到Got here消息几乎立即打印10次,然后由于time.sleep(1)foo来电暂停一段时间。这显然表明迭代器在池进程完成任务之前很久就被完全消耗了。

答案 1 :(得分:1)

基本上,当您迭代文件时,每次从文件中读取新行时,都会将文件指针向前移动一行。

所以,当你这样做时

block=itertools.takewhile(lambda x:x!='STOP',lines) 

每次takewhile返回的迭代器都从lines获取一个新项时,它会移动文件指针。

推进你已经在for循环中循环的迭代器通常很糟糕。但是,for循环暂时在每个yield暂停,maptakewhile循环之前耗尽for,因此您可以获得所需的行为。

for循环和takewhile同时运行时,文件指针会快速移动到最后,然后出错。

请尝试相反,它应该比将takewhile包裹在list中更快:

from contextlib import closing
from itertools import repeat

def block_generator(filename):
    with open(filename) as infile:
        for pos in (infile.tell() for line in infile if line == 'START'):
            yield pos

def my_f_wrapper(pos, filename):
    with open(filename) as infile:
        infile.seek(pos)
        block=itertools.takewhile(lambda x:x!='STOP', infile)
        my_f(block)

blocks = block_generator(filename)
p.imap(my_f_wrapper, blocks, repeat(filename))

基本上,您希望每个my_f在文件上独立运行,因此您需要为每个文件单独打开文件。

我想不出一种方法,不需要将文件迭代两次,一次由for循环,一次由takewhile全部放在一起,同时仍处理该文件并行。在原始版本中,takewhilefor循环提升了文件指针,因此非常有效。

如果你没有迭代线,而只是迭代字节,我建议使用mmap来做这件事,但如果你正在处理文本行,那会使事情变得复杂得多。

修改:另一种方法是让block_generator浏览该文件,找到STARTSTOP的所有位置,然后将它们成对提供包装器。这样,包装器就不必将行与STOP进行比较,只需在文件上使用tell()即可确保它不在STOP。我不确定这是否会更快。