Warum, wann und wie Multithreading und Multiprocessing in Python verwendet werden

Gruß, Chabrowsk. Im Moment hat OTUS ein Set für den Kurs für maschinelles Lernen eröffnet. In diesem Zusammenhang haben wir für Sie ein sehr interessantes „Märchen“ übersetzt. Gehen.




Es war einmal in einer fernen, fernen Galaxie ... Ein

weiser und mächtiger Zauberer lebte in einem kleinen Dorf mitten in der Wüste. Und sie hießen Dumbledalf. Er war nicht nur weise und mächtig, sondern half auch Menschen aus fernen Ländern, einen Zauberer um Hilfe zu bitten. Unsere Geschichte begann, als ein Reisender dem Zauberer eine magische Schriftrolle brachte. Der Reisende wusste nicht, was in der Schriftrolle war, er wusste nur, dass dies Dumbledalf war, wenn jemand alle Geheimnisse der Schriftrolle enthüllen konnte.

Kapitel 1: Single-Threaded, Single-Process


Wenn Sie es noch nicht erraten haben, habe ich eine Analogie zum Prozessor und seinen Funktionen gezogen. Unser Assistent ist ein Prozessor, und eine Schriftrolle ist eine Liste von Links, die dazu führen, dass Python die Fähigkeit und das Wissen besitzt, diese zu beherrschen.

Der erste Gedanke des Zauberers, der die Liste ohne Schwierigkeiten entschlüsselte, war, seinen treuen Freund (Garrigorn? Ich weiß, ich weiß, das klingt schrecklich) an jeden der Orte zu schicken, die in der Schriftrolle beschrieben wurden, um zu finden und zu bringen, was er dort finden konnte.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [3]:
%%time

results = []
for url in urls:
    with urllib.request.urlopen(url) as src:
        results.append(src)

CPU times: user 135 ms, sys: 283 µs, total: 135 ms
Wall time: 12.3 s
In [ ]:

Wie Sie sehen können, durchlaufen wir einfach die URLs einzeln mit der for-Schleife und lesen die Antwort. Dank %% Zeit und der Magie von IPython können wir sehen, dass es mit meinem traurigen Internet ungefähr 12 Sekunden gedauert hat .

Kapitel 2: Multithreading


Es war nicht ohne Grund, dass der Zauberer für seine Weisheit berühmt war, er konnte schnell einen viel effektiveren Weg finden. Anstatt eine Person an jeden Ort zu schicken, sollten Sie eine Gruppe zuverlässiger Mitarbeiter zusammenstellen und sie gleichzeitig in verschiedene Teile der Welt schicken! Der Assistent kann das gesamte Wissen, das er mitbringt, auf einmal vereinen!

Anstatt die Liste nacheinander in einer Schleife anzuzeigen, können wir Multithreading verwenden, um auf mehrere URLs gleichzeitig zuzugreifen.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [4]:
%%time

with ThreadPoolExecutor(4) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 8.27 ms, total: 130 ms
Wall time: 3.83 s
In [5]:
%%time

with ThreadPoolExecutor(8) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 14.7 ms, total: 137 ms
Wall time: 1.79 s
In [6]:
%%time

with ThreadPoolExecutor(16) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 143 ms, sys: 3.88 ms, total: 147 ms
Wall time: 1.32 s
In [ ]:

Viel besser! Fast wie ... Magie. Die Verwendung mehrerer Threads kann viele E / A-Aufgaben erheblich beschleunigen. In meinem Fall ist die meiste Zeit für das Lesen von URLs auf die Netzwerklatenz zurückzuführen. An E / A gebundene Programme verbringen den größten Teil ihres Lebens damit, auf Eingabe oder Ausgabe zu warten (genau wie ein Assistent darauf wartet, dass seine Freunde von der Schriftrolle zu Orten gehen und zurückkehren). Dies kann über das Netzwerk, die Datenbank, die Datei oder vom Benutzer eingegeben / ausgegeben werden. Eine solche E / A nimmt normalerweise viel Zeit in Anspruch, da die Quelle möglicherweise eine Vorverarbeitung durchführen muss, bevor die Daten an die E / A übertragen werden. Beispielsweise zählt ein Prozessor viel schneller als eine Netzwerkverbindung Daten überträgt (mit einer Geschwindigkeit von ungefähr)wie Flash gegen deine Großmutter).

Hinweis : multithreadingKann bei Aufgaben wie dem Bereinigen von Webseiten sehr nützlich sein.

Kapitel 3: Mehrfachverarbeitung


Jahre vergingen, der Ruhm des guten Zauberers wuchs und mit ihm wuchs der Neid eines unparteiischen dunklen Zauberers (Sarumort? Oder vielleicht Volandeman?). Mit unermesslicher List bewaffnet und von Neid getrieben, warf der dunkle Zauberer Dumbledalf einen schrecklichen Fluch auf. Als der Fluch ihn überholte, erkannte Dumbledalf, dass er nur wenige Momente hatte, um ihn abzuwehren. Verzweifelt kramte er in seinen Zauberbüchern und fand schnell einen Gegenzauber, der funktionieren sollte. Das einzige Problem war, dass der Assistent die Summe aller Primzahlen unter 1.000.000 berechnen musste. Ein seltsamer Zauber natürlich, aber was wir haben.

Der Zauberer wusste, dass die Berechnung des Werts trivial sein würde, wenn er genug Zeit hätte, aber er hatte diesen Luxus nicht. Trotz der Tatsache, dass er ein großartiger Zauberer ist, ist er durch seine Menschlichkeit eingeschränkt und kann jeweils nur eine Zahl auf Einfachheit prüfen. Wenn er sich entschied, die Primzahlen einfach nacheinander zu addieren, würde es zu lange dauern. Als nur noch Sekunden vor dem Anwenden des Gegenzaubers übrig waren, erinnerte er sich plötzlich an den Mehrfachverarbeitungszauber , den er vor vielen Jahren aus der magischen Schriftrolle gelernt hatte. Dieser Zauber ermöglicht es ihm, sich selbst zu kopieren, um Zahlen zwischen seinen Kopien zu verteilen und mehrere gleichzeitig zu überprüfen. Und am Ende muss er nur die Zahlen addieren, die er und seine Kopien entdecken werden.

In [1]:
from multiprocessing import Pool
In [2]:
def if_prime(x):
    if x <= 1:
        return 0
    elif x <= 3:
        return x
    elif x % 2 == 0 or x % 3 == 0:
        return 0
    i = 5
    while i**2 <= x:
        if x % i == 0 or x % (i + 2) == 0:
            return 0
        i += 6
    return x
In [17]:
%%time

answer = 0

for i in range(1000000):
    answer += if_prime(i)

CPU times: user 3.48 s, sys: 0 ns, total: 3.48 s
Wall time: 3.48 s
In [18]:
%%time

if __name__ == '__main__':
    with Pool(2) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 114 ms, sys: 4.07 ms, total: 118 ms
Wall time: 1.91 s
In [19]:
%%time

if __name__ == '__main__':
    with Pool(4) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 99.5 ms, sys: 30.5 ms, total: 130 ms
Wall time: 1.12 s
In [20]:
%%timeit

if __name__ == '__main__':
    with Pool(8) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

729 ms ± 3.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [21]:
%%timeit

if __name__ == '__main__':
    with Pool(16) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

512 ms ± 39.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [22]:
%%timeit

if __name__ == '__main__':
    with Pool(32) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

518 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]:
%%timeit

if __name__ == '__main__':
    with Pool(64) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

621 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [ ]:

Moderne Prozessoren haben mehr als einen Kern, sodass wir die Aufgaben mithilfe der Multiprozessverarbeitung des Multiprozessverarbeitungsmoduls beschleunigen können . Die mit dem Prozessor verbundenen Aufgaben sind Programme, mit denen die meiste Zeit Berechnungen am Prozessor durchgeführt werden (triviale mathematische Berechnungen, Bildverarbeitung usw.). Wenn die Berechnungen unabhängig voneinander durchgeführt werden können, haben wir die Möglichkeit, sie auf die verfügbaren Prozessorkerne aufzuteilen, wodurch eine signifikante Erhöhung der Verarbeitungsgeschwindigkeit erzielt wird.

Alles was du tun musst, ist:

  1. Bestimmen Sie, welche Funktion angewendet werden soll
  2. Bereiten Sie eine Liste der Elemente vor, auf die die Funktion angewendet wird.
  3. multiprocessing.Pool. , Pool(), . with , .
  4. Pool map. map , , .

Hinweis : Eine Funktion kann definiert werden, um jede Aufgabe auszuführen, die parallel ausgeführt werden kann. Beispielsweise kann eine Funktion Code enthalten, um das Ergebnis einer Berechnung in eine Datei zu schreiben.

Warum müssen wir uns trennenmultiprocessingundmultithreading? Wenn Sie jemals versucht haben, die Leistung einer Aufgabe auf dem Prozessor mithilfe von Multithreading zu verbessern, ist das Ergebnis genau das Gegenteil. Das ist einfach schrecklich! Mal sehen, wie es passiert ist.

So wie ein Zauberer durch seine menschliche Natur eingeschränkt ist und nur eine Zahl pro Zeiteinheit berechnen kann, wird Python mit einer Funktion namens Global Interpreter Lock (GIL) geliefert . Python lässt Sie gerne so viele Threads erzeugen, wie Sie möchten, aber GILstellt sicher, dass immer nur einer dieser Threads ausgeführt wird.

Bei einer Aufgabe im Zusammenhang mit E / A ist diese Situation völlig normal. Ein Thread sendet eine Anfrage an eine URL und wartet auf eine Antwort. Erst dann kann dieser Thread durch einen anderen ersetzt werden, wodurch eine weitere Anfrage an eine andere URL gesendet wird. Da der Thread nichts tun muss, bis er eine Antwort erhält, spielt es keine Rolle, dass zum angegebenen Zeitpunkt nur ein Thread ausgeführt wird.

Für Aufgaben, die auf dem Prozessor ausgeführt werden, ist das Vorhandensein mehrerer Threads fast so nutzlos wie Nippel in der Panzerung. Da pro Zeiteinheit nur ein Thread ausgeführt werden kann, arbeitet der Prozessor auch mit nur einem Thread, selbst wenn Sie mehrere Threads generieren, denen zur Vereinfachung jeweils eine Nummer zugewiesen wird. Tatsächlich werden diese Nummern immer noch einzeln überprüft. Der Aufwand für die Arbeit mit mehreren Threads verringert die Leistung, die Sie nur bei multithreadingAufgaben auf dem Prozessor beobachten können.

Um diese „Einschränkung“ zu umgehen, verwenden wir das Modulmultiprocessing. Anstatt Threads zu verwenden, verwendet Multiprocessing, wie Sie sagen, mehrere Prozesse. Jeder Prozess erhält seinen eigenen persönlichen Interpreter und Speicherplatz, sodass die GIL Sie nicht einschränkt. Tatsächlich verwendet jeder Prozess seinen eigenen Prozessorkern und arbeitet mit seiner eigenen eindeutigen Nummer. Dies wird gleichzeitig mit der Arbeit anderer Prozesse durchgeführt. Wie süß von ihnen!

Möglicherweise stellen Sie fest, dass die CPU-Auslastung bei Verwendung höher ist als multiprocessingbei einer regulären for-Schleife oder sogar multithreading. Dies geschieht, weil Ihr Programm nicht einen Kern, sondern mehrere verwendet. Und das ist gut!

erinnere dich daranmultiprocessingEs hat seinen eigenen Aufwand für die Verwaltung mehrerer Prozesse, der normalerweise schwerwiegender ist als die Kosten für Multithreading. (Multiprocessing erzeugt separate Interpreter und weist jedem Prozess einen eigenen Speicherbereich zu. Ja!) Das heißt, es ist in der Regel besser, eine Light-Version von Multithreading zu verwenden, wenn Sie auf diese Weise aussteigen möchten (denken Sie an Aufgaben im Zusammenhang mit E / A). Wenn die Berechnung auf dem Prozessor jedoch zu einem Engpass wird, kommt die Modulzeit multiprocessing. Aber denken Sie daran, dass mit großer Kraft große Verantwortung verbunden ist.

Wenn Sie mehr Prozesse erzeugen, als Ihr Prozessor pro Zeiteinheit verarbeiten kann, werden Sie feststellen, dass die Leistung allmählich abnimmt. Dies liegt daran, dass das Betriebssystem mehr Arbeit leisten muss, indem Prozesse zwischen den Prozessorkernen gemischt werden, da mehr Prozesse vorhanden sind. In Wirklichkeit kann alles noch komplizierter sein, als ich Ihnen heute gesagt habe, aber ich habe die Hauptidee vermittelt. Auf meinem System sinkt beispielsweise die Leistung, wenn die Anzahl der Prozesse 16 beträgt. Dies liegt daran, dass mein Prozessor nur 16 logische Kerne enthält.

Kapitel 4: Fazit


  • Bei Aufgaben im Zusammenhang mit E / A multithreadingkann die Leistung verbessert werden.
  • Bei Aufgaben im Zusammenhang mit E / A multiprocessingkann dies ebenfalls die Produktivität steigern, die Kosten sind jedoch in der Regel höher als bei der Verwendung multithreading.
  • Die Existenz der Python-GIL macht uns klar, dass zu einem bestimmten Zeitpunkt in einem Programm nur ein Thread ausgeführt werden kann.
  • Bei Aufgaben im Zusammenhang mit dem Prozessor multithreadingkann die Verwendung die Leistung verringern.
  • Bei prozessorbezogenen Aufgaben multiprocessingkann die Verwendung die Leistung verbessern.
  • Zauberer sind großartig!

Hier werden wir heute unsere Einführung in multithreadingund multiprocessingin Python beenden . Jetzt geh und gewinne!



"Modellierung von COVID-19 mithilfe von Diagrammanalyse und Analyse offener Daten." Kostenlose Lektion.

All Articles