Concurrent.futures vs Multiprocessing in Python 3

148

In Python 3.2 wurden Concurrent Futures eingeführt , bei denen es sich anscheinend um eine erweiterte Kombination der älteren Threading- und Multiprocessing- Module handelt.

Welche Vor- und Nachteile hat die Verwendung für CPU-gebundene Aufgaben gegenüber dem älteren Multiprozessor-Modul?

Dieser Artikel legt nahe, dass es viel einfacher ist, mit ihnen zu arbeiten - ist das der Fall?

GIS-Jonathan
quelle

Antworten:

145

Ich würde nicht concurrent.futures"fortgeschrittener" nennen - es ist eine einfachere Oberfläche, die sehr ähnlich funktioniert, unabhängig davon, ob Sie mehrere Threads oder mehrere Prozesse als zugrunde liegendes Parallelisierungs-Gimmick verwenden.

Wie bei praktisch allen Fällen einer "einfacheren Benutzeroberfläche" sind also die gleichen Kompromisse erforderlich: Sie weist eine flachere Lernkurve auf, zum großen Teil nur, weil so viel weniger zum Lernen verfügbar ist. Da es jedoch weniger Optionen bietet, kann es Sie möglicherweise in einer Weise frustrieren, wie es die umfangreicheren Schnittstellen nicht tun.

Was CPU-gebundene Aufgaben betrifft, ist dies viel zu wenig spezifiziert, um viel aussagekräftiges zu sagen. Für CPU-gebundene Aufgaben unter CPython benötigen Sie mehrere Prozesse anstelle mehrerer Threads, um eine Beschleunigung zu erzielen. Wie viel (wenn überhaupt) Sie beschleunigen, hängt jedoch von den Details Ihrer Hardware, Ihres Betriebssystems und insbesondere davon ab, wie viel prozessübergreifende Kommunikation Ihre spezifischen Aufgaben erfordern. Unter dem Deckmantel basieren alle prozessübergreifenden Parallelisierungs-Gimmicks auf denselben Betriebssystemprimitiven - die API auf hoher Ebene, die Sie verwenden, um diese zu erhalten, ist kein primärer Faktor für die Geschwindigkeit unter dem Strich.

Bearbeiten: Beispiel

Hier ist der endgültige Code, der in dem Artikel gezeigt wird, auf den Sie verwiesen haben, aber ich füge eine Importanweisung hinzu, die erforderlich ist, damit er funktioniert:

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

Hier ist genau das Gleiche mit multiprocessingstattdessen:

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

Beachten Sie, dass die Möglichkeit, multiprocessing.PoolObjekte als Kontextmanager zu verwenden, in Python 3.3 hinzugefügt wurde.

Mit welchem ​​ist es einfacher zu arbeiten? LOL ;-) Sie sind im Wesentlichen identisch.

Ein Unterschied besteht darin, dass Poolso viele verschiedene Methoden unterstützt werden, dass Sie möglicherweise erst erkennen, wie einfach es sein kann , wenn Sie die Lernkurve deutlich nach oben geklettert sind.

Auch hier sind all diese unterschiedlichen Wege sowohl eine Stärke als auch eine Schwäche. Sie sind eine Stärke, da die Flexibilität in einigen Situationen erforderlich sein kann. Sie sind eine Schwäche, weil "vorzugsweise nur ein offensichtlicher Weg, dies zu tun". Ein Projekt, an dem ausschließlich (wenn möglich) concurrent.futuresfestgehalten wird, wird auf lange Sicht wahrscheinlich einfacher zu warten sein, da es keine unbegründete Neuheit in der Verwendung seiner minimalen API gibt.

Tim Peters
quelle
20
"Sie benötigen mehrere Prozesse anstelle mehrerer Threads, um eine Beschleunigung zu erzielen" ist zu hart. Wenn Geschwindigkeit wichtig ist; Der Code verwendet möglicherweise bereits eine C-Bibliothek und kann daher GIL freigeben, z. B. Regex, Lxml, Numpy.
JFS
4
@JFSebastian, danke, dass du das hinzugefügt hast - vielleicht hätte ich "unter reinem CPython" sagen sollen , aber ich fürchte, es gibt keinen kurzen Weg, die Wahrheit hier zu erklären, ohne die GIL zu diskutieren.
Tim Peters
2
Erwähnenswert ist, dass Threads besonders nützlich und ausreichend sein können, wenn Sie mit langen E / A-Vorgängen arbeiten.
Kotrfa
9
@TimPeters In gewisser Weise ProcessPoolExecutortatsächlich mehr Möglichkeiten als hat , Poolweil ProcessPoolExecutor.submitRenditen FutureInstanzen , die Kündigung (erlauben cancel), überprüfen die Exception ausgelöst wurde ( exception), und das Hinzufügen von dynamisch einen Rückruf nach Abschluss aufgerufen werden ( add_done_callback). Keine dieser Funktionen ist für von zurückgegebene AsyncResultInstanzen verfügbar Pool.apply_async. Auf andere Weise Poolgibt es mehr Optionen aufgrund von initializer/ initargs, maxtasksperchildund contextin Pool.__init__und mehr Methoden, die von der PoolInstanz verfügbar gemacht werden.
Max
2
@max, klar, aber beachten Sie, dass es bei der Frage nicht Poolum die Module ging. Poolist ein kleiner Teil dessen, was multiprocessingdrin ist, und befindet sich so weit unten in den Dokumenten, dass es eine Weile dauert, bis die Leute erkennen, dass es überhaupt existiert multiprocessing. Diese spezielle Antwort konzentrierte sich darauf, Pooldass dies der gesamte Artikel ist, mit dem das OP verknüpft ist, und dass cf"viel einfacher zu bearbeiten" einfach nicht wahr ist, was in dem Artikel besprochen wurde. Darüber hinaus, cfist as_completed()auch sehr praktisch sein.
Tim Peters