(c) 2023 Technische Hochschule Augsburg - Fakultät für Informatik - Prof.Dr.Nik Klever - Impressum
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:
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 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.
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]
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:
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:
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
Das Modul multiprocessing unterstützt zwei Arten von Kommunikationskanälen zwischen Prozessen:
Die Klasse Queue ist ein nahes Abbild der Klasse queue.Queue. Queues sind in ihrer Verwendung sowohl Thread als auch Prozess sicher. Zum Beispiel:
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']
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.
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']
%%Mooc MoocMultipleChoiceAssessment
%%Mooc Video
%%Mooc WebReference
https://docs.python.org/3/tutorial/stdlib2.html
Hinweis: Kurze Auflistung weiterer Module aus der Standardbibliothek
%%Mooc WebReference
https://docs.python.org/3/library/threading.html
Hinweis: threading - Thread-basierte Parallelprogrammierung
%%Mooc WebReference
https://docs.python.org/3/library/multiprocessing.html
Hinweis: multiprocessing - Prozess-basierte Parallelprogrammierung