ThreadPoolExecutor внутри ProcessPoolExecutor

я новичок в фьючерсы на модуль и есть задача, которая может выиграть от распараллеливания; но я, похоже, не могу точно выяснить, как настроить функцию для потока и функцию для процесса. Я был бы признателен за любую помощь, которую кто-либо может пролить на этот вопрос.

я оптимизация роя частиц (PSO). Не вдаваясь в подробности о самом PSO, вот базовая компоновка моего кода:

там это Particle класс, с getFitness(self) метод (который вычисляет некоторую метрику и сохраняет ее в self.fitness). Моделирование PSO имеет несколько экземпляров частиц (легко более 10; 100s или даже 1000s для некоторых симуляций).
Время от времени мне приходится вычислять пригодность частиц. В настоящее время я делаю это в for-loop:

for p in listOfParticles:
  p.getFitness(args)
map(lambda p: p.getFitness(args), listOfParticles).

теперь я могу легко сделать это с futures.ProcessPoolExecutor:

with futures.ProcessPoolExecutor() as e:
  e.map(lambda p: p.getFitness(args), listOfParticles)

так как побочные эффекты вызова p.getFitness хранятся в каждой частице, мне не нужно беспокоиться о получении возврата от futures.ProcessPoolExecutor().

пока все хорошо. Но теперь я это замечаю!--11--> создает новые процессы, что означает, что он копирует память, которая является медленной. Я хотел бы иметь возможность делиться памятью-поэтому я должен использовать потоки. Это хорошо и хорошо, пока я не пойму, что запуск нескольких процессов с несколькими потоками внутри каждого процесса, вероятно, будет быстрее, так как несколько потоков все еще работают только на одном процессоре моей сладкой 8-core машины.

вот где я сталкиваюсь с проблемами:
Основываясь на примерах, которые я видел,ThreadPoolExecutor работает на list. Как и ProcessPoolExecutor. Поэтому я не могу сделать ничего итеративного в ProcessPoolExecutor на ферме в ThreadPoolExecutor потому что тогда ThreadPoolExecutor собирается получить один объект для работы (см. Мой попытка, размещенная ниже).
С другой стороны, я не могу нарезать listOfParticles себя, потому что я хочу!--12--> чтобы сделать свою собственную магию, чтобы выяснить, сколько нитей необходимо.

Итак, большой вопрос (наконец-то):
Как я должен структурировать свой код, чтобы я мог эффективно распараллеливать следующие процессы и потоки:

for p in listOfParticles:
  p.getFitness()

2 ответов


я дам вам рабочий код, который смешивает процессы с потоками для решения проблемы, но это не то, что вы ожидаете ;-) Первое, что нужно сделать макет программы, которая не ставит под угрозу ваши реальные данные. Экспериментируйте с чем-нибудь безобидным. Итак, вот начало:

class Particle:
    def __init__(self, i):
        self.i = i
        self.fitness = None
    def getfitness(self):
        self.fitness = 2 * self.i

теперь у нас есть с чем играть. Далее несколько констант:

MAX_PROCESSES = 3
MAX_THREADS = 2 # per process
CHUNKSIZE = 100

Скрипка по вкусу. CHUNKSIZE будет объяснено позже.

первый сюрприз для вас то, что делает моя рабочая функция самого низкого уровня. Это потому, что вы слишком оптимистичны здесь:

так как побочные эффекты вызова p.getFitness хранятся в каждая частица сама по себе, мне не нужно беспокоиться о получении возвращение из будущего.ProcessPoolExecutor().

увы, ничего сделано в рабочем процессе может иметь никакого эффекта на Particle экземпляров в вашей основной программе. Рабочий процесс работает на копии of Particle экземпляры, будь то через реализацию копирования на запись fork() или потому, что он работает над копией, сделанной из unpickling a Particle рассол прошел через процессы.

Итак, если вы хотите, чтобы ваша основная программа посмотреть результаты фитнеса, вам нужно организовать, чтобы отправить информацию обратно в основную программу. Поскольку я недостаточно знаю о вашей фактической программе, здесь я предполагаю, что Particle().i является уникальным целым числом, и что основная программа может легко отображать целые числа назад в Particle экземпляров. Имея это в виду, рабочая функция самого низкого уровня здесь должна возвращать пару: уникальное целое число и результат пригодности:

def thread_worker(p):
    p.getfitness()
    return (p.i, p.fitness)

учитывая это, легко распространить список Particles через потоки и возвращает список (particle_id, fitness) результаты:

def proc_worker(ps):
    import concurrent.futures as cf
    with cf.ThreadPoolExecutor(max_workers=MAX_THREADS) as e:
        result = list(e.map(thread_worker, ps))
    return result

Примечания:

  1. это функция, которую будет запускать каждый рабочий процесс.
  2. я использую Python 3, поэтому используйте list() в законную силу e.map() материализовать все результаты в виде списка.
  3. как упоминалось в комментарии, в CPython распространение задач, связанных с ЦП, по потокам -медленнее чем делать их все в один поток.

осталось только написать код, чтобы распространить список Particles между процессами и получить результаты. Это очень легко сделать с помощью multiprocessing, так что я собираюсь использовать. Я понятия не имею,concurrent.futures может это сделать (учитывая, что мы также смешиваем потоки), но не уход. Но поскольку я даю вам рабочий код, вы можете играть с ним и отчитываться; -)

if __name__ == "__main__":
    import multiprocessing

    particles = [Particle(i) for i in range(100000)]
    # Note the code below relies on that particles[i].i == i
    assert all(particles[i].i == i for i in range(len(particles)))

    pool = multiprocessing.Pool(MAX_PROCESSES)
    for result_list in pool.imap_unordered(proc_worker,
                      (particles[i: i+CHUNKSIZE]
                       for i in range(0, len(particles), CHUNKSIZE))):
        for i, fitness in result_list:
            particles[i].fitness = fitness

    pool.close()
    pool.join()

    assert all(p.fitness == 2*p.i for p in particles)

Примечания:

  1. я нарушаю список Particles в куски "вручную". Вот что!--7--> для. Это потому, что рабочий процесс хочет список of Particles для работы, и в свою очередь это потому, что это то, что futures хочет. Это хорошая идея, чтобы разделить работу независимо, так что вы получите реальный взрыв за доллар в свою очередь для каждого вызова накладные процессов.
  2. imap_unordered() не дает никаких гарантий по поводу порядка, в котором возвращаются результаты. Это дает реализации больше свободы, чтобы организовать работу максимально эффективно. И нас не волнует порядок здесь, так что все в порядке.
  3. обратите внимание, что цикл извлекает (particle_id, fitness) результаты, и изменяет Particle соответственно экземпляров. Возможно, ваш настоящий .getfitness другие мутации Particle экземпляры - не могу догадка. Независимо от этого, основная программа никогда не увидит никаких мутаций, сделанных в рабочих "по волшебству" - вы должны явно это организовать. В пределе вы можете вернуться (particle_id, particle_instance) пары вместо этого, и заменить the Particle случаях в основной программе. Тогда они отражали бы все мутации, сделанные в рабочих процессах.

удачи :-)

фьючерсы полностью вниз

оказывается, это было очень легко заменить multiprocessing. Здесь изменения. Это также (Как упоминалось ранее) заменяет оригинал Particle экземпляры, чтобы захватить все мутации. Здесь есть компромисс: маринование экземпляра требует "намного больше" байтов, чем маринование одного результата "пригодности". Больше сетевого трафика. Выберите свой яд ;-)

для возврата мутированного экземпляра просто требуется заменить последнюю строку thread_worker(), например:

return (p.i, p)

затем заменить все " main" блок с этим:

def update_fitness():
    import concurrent.futures as cf
    with cf.ProcessPoolExecutor(max_workers=MAX_PROCESSES) as e:
        for result_list in e.map(proc_worker,
                      (particles[i: i+CHUNKSIZE]
                       for i in range(0, len(particles), CHUNKSIZE))):
            for i, p in result_list:
                particles[i] = p

if __name__ == "__main__":
    particles = [Particle(i) for i in range(500000)]
    assert all(particles[i].i == i for i in range(len(particles)))

    update_fitness()

    assert all(particles[i].i == i for i in range(len(particles)))
    assert all(p.fitness == 2*p.i for p in particles)

код очень похож на multiprocessor танец. Лично я бы использовал multiprocessing, потому что imap_unordered ценно. Это проблема с упрощенными интерфейсами: они часто покупают простоту ценой сокрытия полезных возможностей.


во-первых, вы уверены, чтобы использовать от запуска нескольких потоков при загрузке всех ваших ядер с процессами? Если он привязан к cpu,вряд ли да. По крайней мере, некоторые тесты должны быть сделаны.

если добавление потоков использует вашу производительность, следующий вопрос заключается в том, можно ли добиться лучшей производительности с ручной балансировкой нагрузки или автоматически. Вручную-я имею в виду тщательное разделение нагрузки на блоки похожие вычислительная сложность и instatiating новый процессор задач на кусок, ваше оринальное, но сомнительное решение. Путем автоматического создания пула процессов / потоков и связи в рабочей очереди для новых задач, к которым вы стремитесь. На мой взгляд, первый подход является одним из парадигм Apache Hadoop, второй реализуется процессорами Works queue, такими как Celery. Первый подход может страдать от того, что некоторые фрагменты задач медленнее и работают, в то время как другие завершены, второй добавляет коммутацию и накладные расходы на ожидание задачи, и это вторая точка необходимо провести эксплуатационные испытания.

наконец, если вы хотите иметь статическую коллекцию процессов с многопоточностью внутри, AFAIK, вы не можете достичь этого с concurrent.futures как есть, и изменить его немного. Я не знаю, есть ли существующие решения для этой задачи, но как concurrent является чистым решением python (без кода C), это можно легко сделать. Рабочий процессор определяется в _adjust_process_count процедура of ProcessPoolExecutor класс, и подклассы и переопределение его с многопоточным подход довольно прямой, вам просто нужно предоставить свой пользовательский _process_worker на основе concurrent.features.thread

Оригинал ProcessPoolExecutor._adjust_process_count для справки:

def _adjust_process_count(self):
    for _ in range(len(self._processes), self._max_workers):
        p = multiprocessing.Process(
                target=_process_worker,
                args=(self._call_queue,
                      self._result_queue))
        p.start()
        self._processes[p.pid] = p