Concurrent.futures vs Multiprocessing w Pythonie 3

Odpowiedzi:

145

Nie nazwałbym concurrent.futuresbardziej „zaawansowanym” - jest to prostszy interfejs, który działa tak samo, niezależnie od tego, czy używasz wielu wątków, czy wielu procesów jako podstawowej sztuczki równoległej.

Tak więc, podobnie jak praktycznie we wszystkich przypadkach „prostszego interfejsu”, występują takie same kompromisy: ma płytszą krzywą uczenia się, w dużej mierze dlatego, że jest o wiele mniej dostępnych do nauczenia; ale ponieważ oferuje mniej opcji, może ostatecznie frustrować Cię w sposób, w jaki bogatsze interfejsy nie będą.

Jeśli chodzi o zadania związane z procesorem, to jest to zbyt mało zdefiniowane, aby powiedzieć, że ma dużo znaczenia. W przypadku zadań związanych z procesorem w CPythonie, potrzebujesz wielu procesów, a nie wielu wątków, aby mieć szansę na przyspieszenie. Ale to, ile (jeśli w ogóle) uzyskasz przyspieszenia, zależy od szczegółów sprzętu, systemu operacyjnego, a zwłaszcza od tego, ile komunikacji między procesami wymagają określone zadania. Pod okładkami wszystkie sztuczki zrównoleglania między procesami opierają się na tych samych prymitywach systemu operacyjnego - interfejs API wysokiego poziomu, którego używasz, nie jest głównym czynnikiem wpływającym na prędkość końcową.

Edycja: przykład

Oto ostateczny kod pokazany w artykule, do którego się odwołałeś, ale dodaję instrukcję importu potrzebną do jej działania:

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))}

Oto dokładnie to samo, używając multiprocessingzamiast tego:

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))}

Zwróć uwagę, że możliwość użycia multiprocessing.Pool obiektów jako menedżerów kontekstu została dodana w Pythonie 3.3.

Z którym łatwiej się pracuje? LOL ;-) Są zasadniczo identyczne.

Jedna różnica polega na tym, że Poolobsługuje tak wiele różnych sposobów robienia rzeczy, że możesz nie zdawać sobie sprawy, jak łatwo może to być być, dopóki nie wespniesz się dość daleko po krzywej uczenia się.

Ponownie, wszystkie te różne sposoby są zarówno mocną, jak i słabą stroną. Są mocną stroną, ponieważ w niektórych sytuacjach może być wymagana elastyczność. Są słabością ze względu na „najlepiej tylko jeden oczywisty sposób na zrobienie tego”. Projekt trzymający się wyłącznie (jeśli to możliwe) concurrent.futuresbędzie prawdopodobnie łatwiejszy w utrzymaniu w dłuższej perspektywie, ze względu na brak darmowej nowości w sposobie wykorzystania jego minimalnego API.

Tim Peters
źródło
20
„potrzebujesz wielu procesów, a nie wielu wątków, aby mieć jakiekolwiek szanse na przyspieszenie” jest zbyt trudne. Jeśli ważna jest prędkość; kod może już korzystać z biblioteki C i dlatego może wydać GIL, np. regex, lxml, numpy.
jfs
4
@JFSebastian, dzięki za dodanie tego - być może powinienem był powiedzieć „w czystym CPythonie”, ale obawiam się, że nie ma tu krótkiego sposobu na wyjaśnienie prawdy bez omawiania GIL.
Tim Peters
2
Warto wspomnieć, że wątki mogą być szczególnie przydatne i wystarczające podczas pracy z długimi IO.
kotrfa
9
@TimPeters Pod pewnymi względami w ProcessPoolExecutorrzeczywistości ma więcej opcji niż Pooldlatego, że ProcessPoolExecutor.submitzwraca Futureinstancje, które umożliwiają cancellation ( cancel), sprawdzanie, który wyjątek został zgłoszony ( exception) i dynamiczne dodawanie wywołania zwrotnego do wywołania po zakończeniu ( add_done_callback). Żadna z tych funkcji nie jest dostępna w AsyncResultprzypadku wystąpień zwróconych przez Pool.apply_async. W inny sposób Poolma więcej opcji tytułu initializer/ initargs, maxtasksperchildoraz contextw Pool.__init__oraz więcej metod narażone przez Poolinstancji.
maksymalnie
2
@max, jasne, ale zauważ, że pytanie nie dotyczyło Pool, chodziło o moduły. Pooljest niewielką częścią tego, co jest w dokumentach multiprocessingi jest tak daleko w dokumentach, że ludzie potrzebują trochę czasu, zanim zdają sobie sprawę, że w ogóle istnieje multiprocessing. Ta konkretna odpowiedź skupiła się na Pooltym, że jest to cały artykuł, do którego odwoływał się program operacyjny, i który cfjest „znacznie łatwiejszy w obsłudze”, po prostu nie jest prawdą w odniesieniu do tego, o czym rozmawialiśmy. Poza tym, cfnic nie as_completed()może być również bardzo przydatny.
Tim Peters