Concurrent.futures против многопроцессорной обработки в Python 3

148

В Python 3.2 было представлено Concurrent Futures , представляющее собой сложную комбинацию старых потоковых и многопроцессорных модулей.

Каковы преимущества и недостатки использования этого для задач, связанных с ЦП, по сравнению со старым многопроцессорным модулем?

Эта статья предполагает, что с ними гораздо проще работать - так ли это?

ГИС-Jonathan
источник

Ответы:

145

Я бы не назвал concurrent.futuresболее «продвинутым» - это более простой интерфейс, который работает практически одинаково, независимо от того, используете ли вы несколько потоков или несколько процессов в качестве основного трюка распараллеливания.

Таким образом, как и практически во всех случаях «более простого интерфейса», в нем присутствуют практически одни и те же компромиссы: он имеет более мелкую кривую обучения, в значительной степени потому, что гораздо меньше доступно для изучения ; но, поскольку он предлагает меньше вариантов, он может в конечном итоге разочаровать вас так, как этого не сделает более богатый интерфейс.

Что касается задач, связанных с ЦП, то это слишком недооценено, чтобы говорить о значении. Для задач с привязкой к процессору в CPython вам нужно несколько процессов, а не несколько потоков, чтобы иметь какой-либо шанс получить ускорение. Но степень (если таковая имеется) ускорения зависит от деталей вашего оборудования, вашей ОС и особенно от того, сколько межпроцессного взаимодействия требуют ваши конкретные задачи. Под прикрытием все уловки межпроцессного распараллеливания основаны на одних и тех же примитивах ОС - высокоуровневый API, который вы используете для достижения этих целей, не является основным фактором, влияющим на конечную скорость.

Изменить: пример

Вот окончательный код, показанный в статье, на которую вы ссылались, но я добавляю оператор импорта, необходимый для его работы:

from concurrent.futures import ProcessPoolExecutor
def pool_factorizer_map(nums, nprocs):
    # Let the executor divide the work among processes by using 'map'.
    with ProcessPoolExecutor(max_workers=nprocs) as executor:
        return {num:factors for num, factors in
                                zip(nums,
                                    executor.map(factorize_naive, nums))}

Вот точно так же, используя multiprocessingвместо этого:

import multiprocessing as mp
def mp_factorizer_map(nums, nprocs):
    with mp.Pool(nprocs) as pool:
        return {num:factors for num, factors in
                                zip(nums,
                                    pool.map(factorize_naive, nums))}

Обратите внимание, что возможность использовать multiprocessing.Poolобъекты в качестве контекстных менеджеров была добавлена ​​в Python 3.3.

С кем легче работать? LOL ;-) Они практически идентичны.

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

Опять же, все эти разные способы являются одновременно и силой, и слабостью. Они сильны, потому что в некоторых ситуациях может потребоваться гибкость. Они слабость из-за «желательно только одного очевидного способа сделать это». Проект, который будет придерживаться исключительно (если возможно) concurrent.futures, вероятно, будет легче поддерживать в долгосрочной перспективе, из-за отсутствия беспричинной новизны в том, как можно использовать его минимальный API.

Тим Питерс
источник
20
«вам нужно несколько процессов, а не несколько потоков, чтобы иметь какие-либо шансы на ускорение» , слишком резко. Если скорость важна; код может уже использовать библиотеку C и поэтому может выпускать GIL, например, regex, lxml, numpy.
JFS
4
@JFSebastian, спасибо за добавление этого - возможно, я должен был сказать «под чистым CPython», но я боюсь, что нет короткого способа объяснить правду здесь, не обсуждая GIL.
Тим Питерс
2
И стоит отметить, что потоки могут быть особенно полезны и достаточны при работе с длинными операциями ввода-вывода.
Котрфа
9
@TimPeters В некоторых отношениях на ProcessPoolExecutorсамом деле имеет больше параметров, чем Poolпотому, что ProcessPoolExecutor.submitвозвращает Futureэкземпляры, которые позволяют cancellation ( cancel), проверять, какое исключение было вызвано ( exception), и динамически добавлять обратный вызов, который будет вызван по завершении ( add_done_callback). Ни одна из этих функций не доступна с AsyncResultэкземплярами, возвращенными Pool.apply_async. В других отношениях Poolимеет больше возможностей из - за initializer/ initargs, maxtasksperchildи contextв Pool.__init__, и больше способов , предоставляемые , Poolнапример.
максимум
2
@max, конечно, но обратите внимание, что вопрос был не о Pool, а о модулях. Poolэто небольшая часть того, что находится внутри multiprocessing, и настолько далеко в документах, что людям нужно время, чтобы осознать, что оно вообще существует multiprocessing. Этот конкретный ответ сфокусирован на Poolтом, что это вся статья, с которой связан ОП, и с которой cf«намного проще работать» просто не соответствует тому, что обсуждалась в статье. Кроме того, cfэто as_completed()также может быть очень удобно.
Тим Питерс