multiprocessing.Pool.imap_unordered与固定队列大小或缓冲区?

时间:2015-05-26 01:58:34

标签: python sqlite generator python-3.4 python-multiprocessing

我正在从大型CSV文件中读取数据,对其进行处理并将其加载到SQLite数据库中。分析表明80%的时间花在I / O上,20%是处理输入以准备数据库插入。我用multiprocessing.Pool加快了处理步骤,以便I / O代码永远不会等待下一条记录。但是,这导致了严重的内存问题,因为I / O步骤无法跟上工作人员的步伐。

以下玩具示例说明了我的问题:

#!/usr/bin/env python  # 3.4.3
import time
from multiprocessing import Pool

def records(num=100):
    """Simulate generator getting data from large CSV files."""
    for i in range(num):
        print('Reading record {0}'.format(i))
        time.sleep(0.05)  # getting raw data is fast
        yield i

def process(rec):
    """Simulate processing of raw text into dicts."""
    print('Processing {0}'.format(rec))
    time.sleep(0.1)  # processing takes a little time
    return rec

def writer(records):
    """Simulate saving data to SQLite database."""
    for r in records:
        time.sleep(0.3)  # writing takes the longest
        print('Wrote {0}'.format(r))

if __name__ == "__main__":
    data = records(100)
    with Pool(2) as pool:
        writer(pool.imap_unordered(process, data, chunksize=5))

此代码会导致最终消耗所有内存的记录积压,因为我无法足够快地将数据持久保存到磁盘。运行代码,当Pool.imap_unordered处于第15条记录左右时,您会注意到writer将消耗所有数据。现在假设处理步骤正在生成数亿行的字典,你可以看到我内存不足的原因。 Amdahl's Law也许在行动中。

对此有何修复?我认为我需要Pool.imap_unordered的某种缓冲区,它说“一旦有 x 记录需要插入,停止并等待,直到少于 x 之前做得更多。“我可以通过准备下一条记录来提高速度,同时保存最后一条记录。

我尝试使用papy模块中的NuMap(我修改后使用Python 3)来完成此操作,但速度并不快。事实上,它比顺序运行程序更糟糕; NuMap使用两个线程加上多个进程。

SQLite的批量导入功能可能不适合我的任务,因为数据需要大量处理和规范化。

我要处理大约85G的压缩文本。我对其他数据库技术持开放态度,但选择SQLite是为了易于使用,因为这是一次写入多次读取的工作,在加载完所有内容后,只有3或4个人将使用生成的数据库。

4 个答案:

答案 0 :(得分:4)

当我处理同样的问题时,我认为防止池过载的有效方法是使用带有生成器的信号量:

from multiprocessing import Pool, Semaphore

def produce(semaphore, from_file):
    with open(from_file) as reader:
        for line in reader:
            # Reduce Semaphore by 1 or wait if 0
            semaphore.acquire()
            # Now deliver an item to the caller (pool)
            yield line

def process(item):
    result = (first_function(item),
              second_function(item),
              third_function(item))
    return result

def consume(semaphore, result):
    database_con.cur.execute("INSERT INTO ResultTable VALUES (?,?,?)", result)
    # Result is consumed, semaphore may now be increased by 1
    semaphore.release()

def main()
    global database_con
    semaphore_1 = Semaphore(1024)
    with Pool(2) as pool:
        for result in pool.imap_unordered(process, produce(semaphore_1, "workfile.txt"), chunksize=128):
            consume(semaphore_1, result)

另见:

K Hong - Multithreading - Semaphore objects & thread pool

Lecture from Chris Terman - MIT 6.004 L21: Semaphores

答案 1 :(得分:2)

由于处理速度很快,但写作速度很慢,听起来就像是问题所在 I / O限制。因此,使用可能无法获得太多 多处理。

然而,可以剥离data的块,处理块,和 等到剥离掉另一个块之后才写入数据:

import itertools as IT
if __name__ == "__main__":
    data = records(100)
    with Pool(2) as pool:
        chunksize = ...
        for chunk in iter(lambda: list(IT.islice(data, chunksize)), []):
            writer(pool.imap_unordered(process, chunk, chunksize=5))

答案 2 :(得分:1)

听起来你真正需要的是用有界(和阻塞)队列替换Pool下面的无界队列。这样,如果任何一方领先于其他方面,它就会阻止,直到他们准备好。

通过窥视the source,子类或monkeypatch Pool可以很容易地做到这一点,例如:

class Pool(multiprocessing.pool.Pool):
    def _setup_queues(self):
        self._inqueue = self._ctx.Queue(5)
        self._outqueue = self._ctx.Queue(5)
        self._quick_put = self._inqueue._writer.send
        self._quick_get = self._outqueue._reader.recv
        self._taskqueue = queue.Queue(10)

但这显然不可移植(即使是CPython 3.3,更不用说不同的Python 3实现了。)

认为你可以通过提供自定义的context在3.4+中进行移植,但我还没有能够做到这一点,所以......

答案 3 :(得分:1)

一个简单的解决方法可能是使用psutil检测每个进程中的内存使用情况,并说是否占用了超过90%的内存,而不仅仅是睡眠一会儿。

while psutil.virtual_memory().percent > 75:
            time.sleep(1)
            print ("process paused for 1 seconds!")