Multithreading

Threading ist eine Technik zur Entkopplung von Aufgaben, die nicht sequentiell abhängig sind. Threads können verwendet werden, um die Reaktionsfähigkeit von Anwendungen zu verbessern, die in einem Thread Benutzereingaben akzeptieren, während andere Aufgaben in Threads im Hintergrund ausgeführt werden. Ein verwandter Anwendungsfall sind Eingabe-/Ausgabe-Operationen die parallel zu Berechnungen in einem anderen Thread ausgeführt werden.

Der folgende Code zeigt, wie das High-Level-Threading-Modul Aufgaben im Hintergrund ausführen kann, während das Hauptprogramm weiterhin läuft:

In [2]:
import threading, zipfile, glob, datetime

class AsyncZip(threading.Thread):
    def __init__(self, infile, outfile):
        threading.Thread.__init__(self)
        self.infile = infile
        self.outfile = outfile

    def run(self):
        f = zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED)
        f.write(self.infile)
        f.close()
        print('{}: Zip-Bearbeitung der Datei {} im Hintergrund fertiggestellt'.format(datetime.datetime.now().time(),self.infile))

for f in glob.glob("../Kap_00_Organisation_und_Einführung/*.ipynb"):
    background = AsyncZip(f, 'myarchive.zip')
    background.start()
print('{}: Das Hauptprogramm läuft im Vordergrund weiter.'.format(datetime.datetime.now().time(),))

background.join()    # Wait for the background task to finish
print('{}: Das Hauptprogramm hat bis zur Beendigung des Hintergrundprogramms gewartet.'.format(datetime.datetime.now().time(),))
11:21:10.419258: Das Hauptprogramm läuft im Vordergrund weiter.
11:21:10.421092: Zip-Bearbeitung der Datei ../Kap_00_Organisation_und_Einführung/NB_01_Einführung.ipynb im Hintergrund fertiggestellt
11:21:10.423100: Zip-Bearbeitung der Datei ../Kap_00_Organisation_und_Einführung/NB_02_Organisation.ipynb im Hintergrund fertiggestellt
11:21:10.423645: Das Hauptprogramm hat bis zur Beendigung des Hintergrundprogramms gewartet.

Die Hauptaufgabe von Multithread-Anwendungen ist die Koordinierung von Threads, die Daten oder andere Ressourcen gemeinsam nutzen. Zu diesem Zweck bietet das Threading-Modul eine Reihe von Synchronisations-Primitiven, einschließlich Sperren, Ereignissen, Zustandsvariablen und Semaphoren.

Während diese Werkzeuge leistungsstark sind, können kleinere Konstruktionsfehler zu Problemen führen, die schwer reproduzierbar sind. Der bevorzugte Ansatz zur Koordination von Threads besteht daher darin, den Zugriff auf eine Ressource in einem einzigen Thread zu konzentrieren und dann das Modul queue zu verwenden, um diesen Thread mit Anfragen von anderen Threads zu füttern. Anwendungen mit Queue-Objekten für Inter-Thread-Kommunikation und Koordination sind einfacher zu entwerfen, besser lesbar und zuverlässiger.

Multiprocessing

multiprocessing ist ein Paket, das Kindprozesse mit einer API erzeugen kann, die dem Modul threading ähnlich ist. Das Paket multiprocessing unterstützt sowohl lokale als auch Remote-Parallelität, die effektiv den Global Interpreter Lock (GIL) durch die Verwendung von Kindprozessen anstelle von Threads umgeht. Der Global Interpreter Lock oder GIL ist in CPython ein Sperrmechanismus, der verhindert, dass mehrere native Threads Python-Bytecodes gleichzeitig ausführen. Diese Sperre ist vor allem deshalb notwendig, weil CPythons Speicherverwaltung nicht threadsicher ist. Aus diesem Grund ermöglicht das Modul multiprocessing dem Programmierer, mehrere Prozessoren auf einem Rechner vollständig auszunutzen.

Das Modul multiprocessing führt auch zusätzliche APIs ein, die kein Analogon im Moduel threading haben. Ein Beispiel dafür ist die Klasse Pool, deren Instanzen eine bequeme Parallelisierung für die Ausführung einer Funktion über mehrere Eingabewerte bietet und die Eingabedaten über entsprechende Kindprozesse verteilt (Datenparallelität). Das folgende Beispiel veranschaulicht diese Praxis der Datenparallelität mittels der Klasse Pool und der Definition entsprechender Funktionen in einem Modul, damit die Kindprozesse diese Funktionen nutzen können.

In [3]:
from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    with Pool(5) as p:
        print(p.map(f, [1, 2, 3]))
[1, 4, 9]

Klasse Process

Beim Multiprocessing werden Prozesse erzeugt, indem ein Prozessobjekt erstellt und dann seine entsprechende start()-Methode aufgerufen wird. Die Klasse Process folgt der API von threading.Thread. Ein triviales Beispiel für ein Multiprozess-Programm ist der folgende Code:

In [4]:
from multiprocessing import Process

def f(name):
    print('hello', name)

if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()
hello bob

Um die einzelnen Prozess-IDs zu zeigen, hier ein erweitertes Beispiel:

In [5]:
from multiprocessing import Process
import os

def info(title):
    print("---Start Info {}---".format(title))
    print('module name:', __name__)
    print('parent process:', os.getppid())
    print('process id:', os.getpid())
    print("---End Info {}---".format(title))

def f(name):
    info('function f')
    print('hello', name)

if __name__ == '__main__':
    print('main process:', os.getpid())
    info('main process')
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()
main process: 2830
---Start Info main process---
module name: __main__
parent process: 1207
process id: 2830
---End Info main process---
---Start Info function f---
module name: __main__
parent process: 2830
process id: 2859
---End Info function f---
hello bob

Austausch von Objekten zwischen Prozessen

Das Modul multiprocessing unterstützt zwei Arten von Kommunikationskanälen zwischen Prozessen:

Queues

Die Klasse Queue ist ein nahes Abbild der Klasse queue.Queue. Queues sind in ihrer Verwendung sowohl Thread als auch Prozess sicher. Zum Beispiel:

In [6]:
from multiprocessing import Process, Queue

def f(q):
    q.put([42, None, 'hello'])

if __name__ == '__main__':
    q = Queue()
    p = Process(target=f, args=(q,))
    p.start()
    print(q.get())    # prints "[42, None, 'hello']"
    p.join()
[42, None, 'hello']

Pipes

Die Klasse Pipe() gibt ein Paar von Verbindungsobjekten zurück, die durch eine Pipe verbunden sind, diese ist standardmäßig im Duplex-Mode (Zwei-Wege-Mode) konfiguriert. Beispielsweise

Die beiden von Pipe() zurückgegebenen Verbindungsobjekte repräsentieren die beiden Enden der Pipe. Jedes Verbindungsobjekt hat u.a. eine send() und recv() Methode. Zu beachten ist, dass Daten in einer Pipe beschädigt werden können, wenn zwei Prozesse (oder Threads) das gleiche Ende der Pipe zur gleichen Zeit versuchen zu lesen oder zu schreiben. Es besteht natürlich keine Gefahr der Beschädigung von Prozessen, wenn mit verschiedenen Enden der Pipe zur gleichen Zeit gearbeitet wird.

In [7]:
from multiprocessing import Process, Pipe

def f(conn):
    conn.send([42, None, 'hello'])
    conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())   # prints "[42, None, 'hello']"
    p.join()
[42, None, 'hello']
In [8]:
%%Mooc MoocMultipleChoiceAssessment
Out[8]:

Multithreading vs. Multiprocessing

Was ist der Unterschied zwischen Multithreading und Multiprocessing

Es gibt keinen
Alle Threads in einem Multithreading-Prozess laufen im selben Prozess ab
Alle Prozesse in einem Multiprocessing-Prozess laufen im selben Prozess ab

In [9]:
%%Mooc Video
Out[9]:

Weitere Literatur

In [10]:
%%Mooc WebReference

Brief Tour of the Standard Library - Part II

https://docs.python.org/3/tutorial/stdlib2.html

Hinweis: Kurze Auflistung weiterer Module aus der Standardbibliothek

In [11]:
%%Mooc WebReference

threading — Thread-based parallelism

https://docs.python.org/3/library/threading.html

Hinweis: threading - Thread-basierte Parallelprogrammierung

In [12]:
%%Mooc WebReference

multiprocessing — Process-based parallelism

https://docs.python.org/3/library/multiprocessing.html

Hinweis: multiprocessing - Prozess-basierte Parallelprogrammierung