如何使用所有CPU对大量文件进行子处理?

时间:2019-02-16 22:06:11

标签: python multiprocessing subprocess pool python-3.7

我需要在命令行中使用LaTeXML库将86,000 TEX文件转换为XML。我试图编写一个Python脚本来利用subprocess模块,利用所有4个内核来自动执行此操作。

def get_outpath(tex_path):
    path_parts = pathlib.Path(tex_path).parts
    arxiv_id = path_parts[2]
    outpath = 'xml/' + arxiv_id + '.xml'
    return outpath

def convert_to_xml(inpath):
    outpath = get_outpath(inpath)

    if os.path.isfile(outpath):
        message = '{}: Already converted.'.format(inpath)
        print(message)
        return

    try:
        process = subprocess.Popen(['latexml', '--dest=' + outpath, inpath], 
                                   stderr=subprocess.PIPE, 
                                   stdout=subprocess.PIPE)
    except Exception as error:
        process.kill()
        message = "error: %s run(*%r, **%r)" % (e, args, kwargs)
        print(message)

    message = '{}: Converted!'.format(inpath)
    print(message)

def start():
    start_time = time.time()
    pool = multiprocessing.Pool(processes=multiprocessing.cpu_count(),
                               maxtasksperchild=1)
    print('Initialized {} threads'.format(multiprocessing.cpu_count()))
    print('Beginning conversion...')
    for _ in pool.imap_unordered(convert_to_xml, preprints, chunksize=5): 
        pass
    pool.close()
    pool.join()
    print("TIME: {}".format(total_time))

start()

该脚本生成Too many open files并降低计算机速度。从活动监视器看,该脚本似乎试图一次创建86,000个转换子进程,并且每个进程都试图打开文件。也许这是pool.imap_unordered(convert_to_xml, preprints)的结果-也许我不需要将map与subprocess.Popen结合使用,因为我要调用的命令太多了?有什么选择吗?

我整天都在努力寻找正确的方法来进行批量子处理。我是Python的新手,所以向正确方向前进的任何提示将不胜感激。谢谢!

1 个答案:

答案 0 :(得分:3)

convert_to_xml中,process = subprocess.Popen(...)语句产生一个latexml子进程。 没有process.communicate()之类的阻塞调用,即使convert_to_xml继续在后台运行,latexml也会结束。

convert_to_xml结束以来,池向关联的工作进程发送了另一个要运行的任务,因此convert_to_xml被再次调用。 再次在后台生成另一个latexml进程。 很快,您将在latexml进程中全神贯注,并且已达到打开文件数量的资源限制。

解决方法很简单:添加process.communicate()告诉convert_to_xml等到latexml进程完成。

try:
    process = subprocess.Popen(['latexml', '--dest=' + outpath, inpath], 
                               stderr=subprocess.PIPE, 
                               stdout=subprocess.PIPE)
    process.communicate()                                   
except Exception as error:
    process.kill()
    message = "error: %s run(*%r, **%r)" % (e, args, kwargs)
    print(message)

else: # use else so that this won't run if there is an Exception
    message = '{}: Converted!'.format(inpath)
    print(message)

关于if __name__ == '__main__'

作为martineau pointed out,有一个warning in the multiprocessing docs 不应在模块的顶层调用产生新进程的代码。 相反,该代码应包含在if __name__ == '__main__'语句中。

在Linux中,如果忽略此警告,则不会发生任何可怕的事情。 但是在Windows中,代码为“ fork-bombs”。或更准确地说,代码 导致生成未缓解的子流程链,因为在Windows fork上通过生成新的Python流程进行仿真,然后导入调用脚本。每次导入都会产生一个新的Python进程。每个Python进程都会尝试导入调用脚本。直到消耗完所有资源后,该周期才结束。

为了对我们的Windows-fork-bereft弟兄友善,请使用

if __name__ == '__main__:
    start()

有时进程需要大量内存。 The only reliable way释放内存是为了终止该过程。 maxtasksperchild=1告诉pool完成每个任务后终止每个工作进程。然后,它产生一个新的工作进程来处理另一个任务(如果有的话)。这样可以释放原始工作线程可能已经分配的(内存)资源,而这些资源原本无法释放的。

在您的情况下,工作进程似乎不需要大量内存,因此您可能不需要maxtasksperchild=1。 在convert_to_xml中,process = subprocess.Popen(...)语句产生一个latexml子进程。 没有process.communicate()之类的阻塞调用,即使convert_to_xml继续在后台运行,latexml也会结束。

convert_to_xml结束以来,池向关联的工作进程发送了另一个要运行的任务,因此convert_to_xml被再次调用。 再次在后台生成另一个latexml进程。 很快,您将在latexml进程中全神贯注,并且已达到打开文件数量的资源限制。

解决方法很简单:添加process.communicate()告诉convert_to_xml等到latexml进程完成。

try:
    process = subprocess.Popen(['latexml', '--dest=' + outpath, inpath], 
                               stderr=subprocess.PIPE, 
                               stdout=subprocess.PIPE)
    process.communicate()                                   
except Exception as error:
    process.kill()
    message = "error: %s run(*%r, **%r)" % (e, args, kwargs)
    print(message)

else: # use else so that this won't run if there is an Exception
    message = '{}: Converted!'.format(inpath)
    print(message)

chunksize影响工作人员在将结果发送回主流程之前执行的任务数量。 Sometimes这可能会影响性能,尤其是在进程间通信是整个运行时的重要部分的情况下。

在您的情况下,convert_to_xml将花费相对较长的时间(假设我们等到latexml完成),并且它仅返回None。因此,进程间通信可能不是整个运行时的重要部分。因此,我不希望您在这种情况下会发现性能发生重大变化(尽管进行实验永远不会受伤!)。


在普通的Python中,map不应仅用于多次调用函数。

出于类似的风格原因,我会在关心返回值的情况下使用pool.*map*方法。

所以不是

for _ in pool.imap_unordered(convert_to_xml, preprints, chunksize=5): 
    pass

您可能会考虑使用

for preprint in preprints:
    pool.apply_async(convert_to_xml, args=(preprint, ))

相反。


传递给任何pool.*map*函数的iterable被消耗。 立即。迭代器是否为迭代器并不重要。没有 使用此处的迭代器具有特殊的内存优势。 imap_unordered 返回 迭代器,但是它不会以任何特别对迭代器友好的方式处理其输入 办法。

无论您传递什么类型的可迭代对象,在调用pool.*map*函数时,可迭代对象都是 消耗并转化为任务,然后将其放入任务队列。

以下代码可证实这一说法:

version1.py:

import multiprocessing as mp
import time

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


def gen():
    for x in range(1000):
        if x % 100 == 0:
            print('Got here')
        yield x


def start():
    pool = mp.Pool()
    for item in pool.imap_unordered(foo, gen()):
        pass

    pool.close()
    pool.join()

if __name__ == '__main__':
    start()

version2.py:

import multiprocessing as mp
import time
def foo(x):
    time.sleep(0.1)
    return x * x


def gen():
    for x in range(1000):
        if x % 100 == 0:
            print('Got here')
        yield x


def start():
    pool = mp.Pool()

    for item in gen():
        result = pool.apply_async(foo, args=(item, ))

    pool.close()
    pool.join()

if __name__ == '__main__':
    start()

运行version1.pyversion2.py都会产生相同的结果。

Got here
Got here
Got here
Got here
Got here
Got here
Got here
Got here
Got here
Got here

至关重要的是,您会发现Got here在以下位置非常快地打印了10次 运行的开始,然后有很长的暂停(在计算时 完成))。

如果gen()以某种方式缓慢消耗了生成器pool.imap_unordered, 我们应该期望Got here也能缓慢打印。由于Got here是 快速打印了10次,我们可以看到正在迭代的gen() 在任务完成之前就完全消耗掉了。

运行这些程序有望使您充满信心 pool.imap_unorderedpool.apply_async正在将任务放入队列 基本上以相同的方式:拨打电话后立即使用。