217 Stimmen

Was sind die Unterschiede zwischen den Threading- und Multiprocessing-Modulen?

Ich lerne, wie man die threading und die multiprocessing Module in Python verwendet, um bestimmte Operationen parallel auszuführen und meinen Code zu beschleunigen.

Ich finde es schwierig (vielleicht weil ich keine theoretischen Kenntnisse darüber habe) zu verstehen, was der Unterschied zwischen einem threading.Thread() Objekt und einem multiprocessing.Process() Objekt ist.

Außerdem ist es mir nicht ganz klar, wie ich eine Warteschlange von Aufgaben instantiieren kann und nur 4 (zum Beispiel) von ihnen parallel laufen lassen kann, während die anderen auf die Freigabe von Ressourcen warten, bevor sie ausgeführt werden.

Die Beispiele in der Dokumentation finde ich klar, aber nicht sehr umfassend; sobald ich versuche, die Dinge etwas zu komplizieren, erhalte ich viele seltsame Fehler (wie eine Methode, die nicht gepickt werden kann, usw.).

Also, wann sollte ich die threading und multiprocessing Module verwenden?

Können Sie mir einige Ressourcen verlinken, die die Konzepte hinter diesen beiden Modulen erläutern und wie man sie richtig für komplexe Aufgaben verwendet?

352voto

abarnert Punkte 332066

Was Giulio Franco sagt, trifft im Allgemeinen auf Multithreading vs. Multiprocessing zu.

Allerdings hat Python* ein zusätzliches Problem: Es gibt ein globales Interpreter-Sperrobjekt, das verhindert, dass zwei Threads im selben Prozess gleichzeitig Python-Code ausführen. Das bedeutet, wenn Sie 8 Kerne haben und Ihren Code ändern, um 8 Threads zu verwenden, wird er nicht in der Lage sein, 800 % CPU zu nutzen und 8-mal schneller zu laufen; er wird die gleichen 100 % CPU verwenden und mit der gleichen Geschwindigkeit laufen. (In der Realität wird es etwas langsamer laufen, weil es zusätzlichen Overhead durch das Threading gibt, auch wenn Sie keine gemeinsamen Daten haben, aber ignorieren Sie das jetzt.)

Davon gibt es Ausnahmen. Wenn die intensive Berechnung Ihres Codes tatsächlich nicht in Python stattfindet, sondern in einer Bibliothek mit benutzerdefinierter C-Code-Implementierung, die das GIL richtig behandelt, wie zum Beispiel eine Numpy-App, erhalten Sie den erwarteten Leistungsvorteil durch Threading. Das Gleiche gilt, wenn die intensive Berechnung von einem Unterprozess durchgeführt wird, den Sie starten und auf den Sie warten.

Viel wichtiger ist jedoch, dass es Fälle gibt, in denen dies keine Rolle spielt. Beispielsweise verbringt ein Netzwer-Server die meiste Zeit damit, Pakete aus dem Netzwerk zu lesen, und eine GUI-App wartet die meiste Zeit auf Benutzerereignisse. Ein Grund, Threads in einem Netzwerk-Server oder einer GUI-App zu verwenden, besteht darin, länger laufende "Hintergrundaufgaben" durchführen zu können, ohne den Hauptthread daran zu hindern, weiterhin Netzwerkpakete oder GUI-Ereignisse zu verarbeiten. Und das funktioniert mit Python-Threads einwandfrei. (In technischer Hinsicht bedeuten Python-Threads also Parallelität, obwohl sie keine Kerne-Parallelität bieten.)

Aber wenn Sie ein CPU-gebundenes Programm in reinem Python schreiben, sind mehr Threads im Allgemeinen nicht hilfreich.

Die Verwendung separater Prozesse hat keine solchen Probleme mit dem GIL, da jeder Prozess seinen eigenen separaten GIL hat. Natürlich haben Sie immer noch alle gleichen Tradeoffs zwischen Threads und Prozessen wie in anderen Sprachen - es ist schwieriger und teurer, Daten zwischen Prozessen als zwischen Threads zu teilen, es kann teuer sein, eine große Anzahl von Prozessen auszuführen oder sie häufig zu erstellen und zu zerstören etc. Aber das GIL wirkt sich in einem Maße auf die Balance zugunsten von Prozessen aus, das für andere Sprachen wie C oder Java nicht zutrifft. Also werden Sie häufiger Multiprocessing in Python verwenden als in C oder Java.


Unterdessen bringt die "Batterien enthalten" Philosophie von Python einige gute Nachrichten: Es ist sehr einfach, Code zu schreiben, der mit einem Einzeiler zwischen Threads und Prozessen umgeschaltet werden kann.

Wenn Sie Ihren Code in Form von eigenständigen "Jobs" entwerfen, die nichts mit anderen Jobs (oder dem Hauptprogramm) teilen außer Eingabe und Ausgabe, können Sie die concurrent.futures Bibliothek verwenden, um Ihren Code um einen Threadpool wie folgt zu schreiben:

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    executor.submit(job, argument)
    executor.map(some_function, collection_of_independent_things)
    # ...

Sie können sogar die Ergebnisse dieser Jobs erhalten und sie an weitere Jobs weitergeben, auf Dinge in der Reihenfolge der Ausführung oder der Fertigstellung warten usw.; lesen Sie den Abschnitt zu Future-Objekten für Details.

Wenn sich herausstellt, dass Ihr Programm ständig 100 % CPU verwendet und das Hinzufügen von mehr Threads es nur langsamer macht, dann stoßen Sie auf das GIL-Problem, und Sie müssen zu Prozessen wechseln. Alles, was Sie tun müssen, ist diese erste Zeile zu ändern:

with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:

Der einzige wirkliche Haken ist, dass die Argumente und Rückgabewerte Ihrer Jobs picklebar sein müssen (und nicht zu viel Zeit oder Speicher benötigen, um gepickelt zu werden), um sie über Prozesse hinweg verwenden zu können. Normalerweise ist das kein Problem, aber manchmal kommt es vor.


Was aber, wenn Ihre Jobs nicht eigenständig sein können? Wenn Sie Ihren Code in Form von Jobs entwerfen können, die Nachrichten von einem an einen anderen weitergeben, ist es immer noch ziemlich einfach. Möglicherweise müssen Sie threading.Thread oder multiprocessing.Process verwenden, anstelle von Pools zu verlassen. Und Sie müssen queue.Queue oder multiprocessing.Queue-Objekte explizit erstellen. (Es gibt viele andere Optionen - Pipes, Sockets, Dateien mit Locks, ... aber der Punkt ist, Sie müssen manuell etwas tun, wenn die automatische Magie eines Executors nicht ausreicht.)

Aber was ist, wenn Sie sich nicht einmal auf den Nachrichtenaustausch verlassen können? Was ist, wenn Sie möchten, dass zwei Jobs dieselbe Struktur verändern und die Änderungen des anderen sehen? In diesem Fall müssen Sie manuelle Synchronisation (Sperren, Semaphoren, Bedingungen usw.) und, wenn Sie Prozesse verwenden möchten, explizite gemeinsam genutzte Speicherobjekte verwenden. Hier wird Multithreading (oder Multiprocessing) schwierig. Wenn Sie es vermeiden können, umso besser; wenn nicht, müssen Sie mehr lesen als jemand in eine SO-Antwort packen kann.


In einem Kommentar wollten Sie wissen, was in Python der Unterschied zwischen Threads und Prozessen ist. Wenn Sie die Antwort von Giulio Franco, meine und alle unsere Links lesen, sollten Sie alles abgedeckt haben ... aber eine Zusammenfassung wäre definitiv nützlich, also los geht's:

  1. Threads teilen standardmäßig Daten; Prozesse nicht.
  2. Aus Folge von (1) erfordert das Senden von Daten zwischen Prozessen in der Regel deren Ein- und Auspacken.
  3. Auch aus Folge von (1) erfordert das direkte Teilen von Daten zwischen Prozessen im Allgemeinen, dass es in Rohdatenformaten wie "Value", "Array" und ctypes-Typen abgelegt wird.
  4. Prozesse sind nicht dem GIL unterworfen.
  5. Auf einigen Plattformen (hauptsächlich Windows) sind Prozesse wesentlich teurer zu erstellen und zu zerstören.
  6. Es gibt einige zusätzliche Einschränkungen für Prozesse, die auf verschiedenen Plattformen unterschiedlich sind. Details finden Sie in den Programmierungsrichtlinien.
  7. Das threading-Modul verfügt nicht über einige der Funktionen des multiprocessing-Moduls. (Sie können multiprocessing.dummy verwenden, um einen Großteil der fehlenden API auf Basis von Threads zu erhalten, oder Sie können höherstufige Module wie concurrent.futures verwenden und sich keine Gedanken darüber machen.)

* Es ist nicht wirklich Python, die Sprache, die dieses Problem hat, sondern CPython, die "Standard"-Implementierung dieser Sprache. Einige andere Implementierungen haben keinen GIL, wie zum Beispiel Jython.

** Wenn Sie die "fork"-Startmethode für Multiprocessing verwenden - was Sie auf den meisten nicht-Windows-Plattformen können - erhält jeder Kindprozess alle Ressourcen, die der Elternprozess hatte, als das Kind gestartet wurde, was eine andere Möglichkeit darstellt, Daten an Kinder weiterzugeben.

53voto

Giulio Franco Punkte 3170

Mehrere Threads können in einem einzigen Prozess existieren. Die Threads, die zu demselben Prozess gehören, teilen sich denselben Speicherbereich (können von denselben Variablen lesen und in sie schreiben und sich gegenseitig beeinflussen). Im Gegensatz dazu leben verschiedene Prozesse in verschiedenen Speicherbereichen, und jeder von ihnen hat seine eigenen Variablen. Um zu kommunizieren, müssen Prozesse andere Kanäle verwenden (Dateien, Pipes oder Sockets).

Wenn Sie eine Berechnung parallelisieren möchten, benötigen Sie wahrscheinlich Multithreading, weil Sie wahrscheinlich möchten, dass die Threads im selben Speicher zusammenarbeiten.

Im Hinblick auf die Leistung sind Threads schneller zu erstellen und zu verwalten als Prozesse (weil das Betriebssystem keinen neuen virtuellen Speicherbereich benötigt), und die Kommunikation zwischen Threads ist in der Regel schneller als die Kommunikation zwischen Prozessen. Aber Threads sind schwerer zu programmieren. Threads können sich gegenseitig beeinflussen und in den Speicher des anderen schreiben, aber der Weg, auf dem dies geschieht, ist nicht immer offensichtlich (aufgrund mehrerer Faktoren, hauptsächlich Instruktionen-Neuordnung und Speicher-Caching), und daher benötigen Sie Synchronisationsprimitive, um den Zugriff auf Ihre Variablen zu kontrollieren.

17voto

Python-Dokumentationszitate

Ich habe die wichtigsten Python-Dokumentationszitate zu Prozessen vs. Threads und dem GIL markiert unter: Was ist das globale Interpreter-Lock (GIL) in CPython?

Prozess vs. Thread-Experimente

Ich habe ein paar Benchmark-Tests durchgeführt, um den Unterschied anschaulicher zu machen.

In dem Benchmark habe ich CPU- und IO-gebundene Arbeiten für verschiedene Anzahlen von Threads auf einer 8 Hyperthread CPU getaktet. Die Arbeit, die pro Thread geleistet wird, ist immer die gleiche, sodass mehr Threads mehr Gesamtarbeit bedeuten.

Die Ergebnisse waren:

Bildbeschreibung hier eingeben

Daten plotten.

Schlussfolgerungen:

  • für CPU-gebundene Arbeit ist multiprocessing immer schneller, vermutlich aufgrund des GIL

  • für IO-gebundene Arbeit sind beide genau gleich schnell

  • Threads skalieren nur bis ungefähr 4x anstatt der erwarteten 8x, da ich mich auf einer 8-Hyperthread-Maschine befinde.

    Im Gegensatz dazu erreicht eine C POSIX CPU-gebundene Arbeit die erwartete 8x-Beschleunigung: Was bedeuten 'real', 'user' und 'sys' in der Ausgabe von time(1)?

    TODO: Ich kenne den Grund dafür nicht, es müssen andere Python-Unzulänglichkeiten im Spiel sein.

Testcode:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    Eine nutzlose CPU-gebundene Funktion.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHub-Upstream + Plot-Code im gleichen Verzeichnis.

Getestet auf Ubuntu 18.10, Python 3.6.7, auf einem Lenovo ThinkPad P51 Laptop mit CPU: Intel Core i7-7820HQ CPU (4 Kerne / 8 Threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ-000L7 (3.000 MB/s).

Visualisierung, welche Threads zu einem bestimmten Zeitpunkt ausgeführt werden

Dieser Beitrag https://rohanvarma.me/GIL/ hat mir beigebracht, dass man eine Rückruffunktion ausführen kann, wenn ein Thread mit dem target=-Argument von threading.Thread und dasselbe für multiprocessing.Process geplant wird.

Dies ermöglicht es uns genau zu sehen, welcher Thread zu welchem Zeitpunkt läuft. Wenn dies gemacht wird, würden wir etwas wie folgt sehen (ich habe dieses bestimmte Diagramm erstellt):

            +--------------------------------------+
            + Aktive Threads / Prozesse            +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Prozess  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Zeit -->                             +
            +--------------------------------------+

was zeigt, dass:

  • Threads vollständig durch den GIL serialisiert sind
  • Prozesse parallel laufen können

5voto

ehfaafzv Punkte 181

Ich glaube, dass dieser Link Ihre Frage auf elegante Weise beantwortet.

Um es kurz zu machen, wenn eines Ihrer Teilprobleme warten muss, während ein anderes endet, ist Multithreading gut (zum Beispiel bei E/A-intensiven Operationen); im Gegensatz dazu, wenn Ihre Teilprobleme wirklich gleichzeitig auftreten könnten, wird Multiprocessing empfohlen. Sie werden jedoch nicht mehr Prozesse erstellen als Ihre Anzahl an Kernen.

2voto

Mario Aguilera Punkte 1146

Hier sind einige Leistungsdaten für Python 2.6.x, die die Vorstellung in Frage stellen, dass Threading in IO-gebundenen Szenarien performanter ist als Multiprocessing. Diese Ergebnisse stammen von einem 40-Prozessor IBM System x3650 M4 BD.

IO-Bound Processing: Prozesspool schnitt besser ab als Thread-Pool

>>> do_work(50, 300, 'thread','fileio')
do_work Funktion dauerte 455.752 ms

>>> do_work(50, 300, 'process','fileio')
do_work Funktion dauerte 319.279 ms

CPU-Bound Processing: Prozesspool schnitt besser ab als Thread-Pool

>>> do_work(50, 2000, 'thread','square')
do_work Funktion dauerte 338.309 ms

>>> do_work(50, 2000, 'process','square')
do_work Funktion dauerte 287.488 ms

Dies sind keine rigorosen Tests, aber sie zeigen mir, dass Multiprocessing im Vergleich zu Threading nicht vollständig unperformant ist.

Code, der in der interaktiven Python-Konsole für die obigen Tests verwendet wurde

from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob

text_for_test = str(range(1,100000))

def fileio(i):
 try :
  os.remove(glob('./test/test-*'))
 except : 
  pass
 f=open('./test/test-'+str(i),'a')
 f.write(text_for_test)
 f.close()
 f=open('./test/test-'+str(i),'r')
 text = f.read()
 f.close()

def square(i):
 return i*i

def timing(f):
 def wrap(*args):
  time1 = time.time()
  ret = f(*args)
  time2 = time.time()
  print '%s Funktion dauerte %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
  return ret
 return wrap

result = None

@timing
def do_work(process_count, items, process_type, method) :
 pool = None
 if process_type == 'process' :
  pool = Pool(processes=process_count)
 else :
  pool = ThreadPool(processes=process_count)
 if method == 'square' : 
  multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]
 else :
  multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]

do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')

do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X