multiprocessing.Pool.imap_unordered с фиксированным размером очереди или буфера?

5

Я читаю данные из больших CSV файлов, обрабатываю их и загружаю в базу данных SQLite. Профилирование предполагает, что 80% моего времени тратится на ввод-вывод, а 20% обрабатывает ввод, чтобы подготовить его для вставки БД. Я ускорил шаг обработки с помощью multiprocessing.Pool, чтобы код ввода/вывода никогда не ожидал следующей записи. Но это вызвало серьезные проблемы с памятью, потому что шаг ввода-вывода не мог идти в ногу с рабочими.

Следующий пример игрушки иллюстрирует мою проблему:

#!/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 будет потреблять все данные, когда writer находится на 15-й записи или около того. Теперь представьте, что на этапе обработки производятся словари из сотен миллионов строк, и вы можете понять, почему у меня заканчивается память. Amdahl Law в действии возможно.

Какое исправление для этого? Я думаю, мне нужен какой-то буфер для Pool.imap_unordered, который гласит: "Когда есть x-записи, которые требуют вставки, останавливаются и ждут, пока их не будет меньше x, прежде чем создавать больше". Я должен быть в состоянии получить некоторое улучшение скорости от подготовки следующей записи, пока последняя сохраняется.

Я попытался использовать NuMap из модуля papy (который я модифицировал для работы с Python 3), чтобы сделать именно это, но это было не быстрее. Фактически, это было хуже, чем запуск программы последовательно; NuMap использует два потока плюс несколько процессов.

Функции массового импорта SQLite, вероятно, не подходят для моей задачи, потому что данные требуют существенной обработки и нормализации.

У меня есть около 85G сжатого текста для обработки. Я открыт для других технологий баз данных, но выбрал SQLite для удобства использования и потому, что это однократное чтение-многозадачное задание, в котором только 3 или 4 человека будут использовать полученную базу данных после того, как все будет загружено.

  • 0
    Используя семафорный подход из этого ответа , я создал оболочку для итератора, чтобы ограничить число полученных значений. pypi.python.org/pypi/bounded-iterator
Теги:
generator
python-multiprocessing
python-3.4

4 ответа

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
    Это кажется лучшим решением. Это компромисс между нарушением синхронизации процессов и повышением скорости на этапе обработки. Было бы неплохо иметь multiprocessing функцию, которая выполняет imap с каким-то параметром буфера.
2

Поскольку я работал над одной и той же проблемой, я решил, что эффективным способом предотвращения переполнения пула является использование семафора с генератором:

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(semaphore1, result)

См. также:

K Hong - многопоточность - объекты семафора и пул потоков

Лекция от Криса Термана - MIT 6.004 L21: Семафоры

0

Используя подход Семафор от этого ответа, я создал оболочку для итератора, чтобы ограничить количество полученных значений.

https://pypi.python.org/pypi/bounded-iterator/

0

Похоже, все, что вам действительно нужно, - это заменить неограниченные очереди под Pool ограниченными (и блокирующими) очередями. Таким образом, если какая-либо сторона опередит остальных, она просто блокируется, пока не будет готова.

Это было бы легко сделать, просмотрев источник, в подкласс или 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).

Я думаю, что вы можете сделать это переносимо в 3.4+, предоставив настраиваемый context, но я не смог получить это право, поэтому...

  • 0
    Это интересная идея, которая не работает просто на практике. _taskqueue фактически назначается после _setup_queues . Помещение максимального размера в очередь также не работает из-за структуры imap_unordered , которая никогда не вернет генератор, потому что очередь блокируется, если установлен максимальный размер.
  • 0
    @ChrisP: Ну, первую часть легко исправить, просто немного грязнее. Второе, правда, ты прав. Я слишком много думал о том, как реализованы функции карты. Это было бы легче построить поверх concurrent.futures.ProcessPoolExecutor; on top of многопроцессорности. Пул , you have to basically rewrite the функции карты с нуля поверх отдельных представлений…

Ещё вопросы

Сообщество Overcoder
Наверх
Меню