Aufwärmen · Aufgabe 1 von 7
// A3.2 · ca. 30 Min. · Vertiefung
Prozesse mit multiprocessing
Nach dieser Lektion verteilen Sie CPU-lastige Arbeit mit Process und Pool.map auf mehrere Kerne, schützen den Einstiegspunkt und übergeben nur Daten, die ein Kindprozess empfangen kann.
Danach können Sie
- Funktionen mit Process und Pool.map in Kindprozessen ausführen und Ergebnisse und Exit-Codes auslesen
- Den Einstiegspunkt mit if __name__ == "__main__" schützen und erklären, warum spawn und forkserver das brauchen
- Picklebare Argumente und Funktionen auf Modulebene übergeben, im Wissen, dass ein Kind mit Kopien arbeitet
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen. Der Kindprozess addiert 100 zu einer globalen Variable. Was gibt der Elternprozess aus?
from multiprocessing import Process counter = 0 def bump() -> None: global counter counter += 100 if __name__ == "__main__": p = Process(target=bump) p.start() p.join() print(counter, p.exitcode)Üben · Aufgabe 3 von 7
Vervollständigen Sie den Guard, damit der Pool nur entsteht, wenn die Datei als Programm läuft, nicht wenn ein Kindprozess sie importiert.
from multiprocessing import Pool def square(x: int) -> int: return x * x if __name__ == ____: with Pool(2) as pool: print(pool.map(square, [1, 2, 3]))if __name__ == :Üben · Aufgabe 4 von 7
Ein Aufruf im Pool löst ZeroDivisionError aus. Was gibt das aus?
from multiprocessing import Pool def inverse(x: int) -> float: return 1 / x if __name__ == "__main__": with Pool(2) as pool: try: pool.map(inverse, [1, 0, 2]) except ZeroDivisionError as error: print("caught:", error)Üben · Aufgabe 5 von 7
Ordnen Sie jedem Baustein von multiprocessing zu, was er Ihnen liefert.
Denksport · Aufgabe 6 von 7
Knobelaufgabe. Das Kind hängt an die Liste an, die es bekommen hat. Was gibt der Elternprozess aus?
from multiprocessing import Process def add(items: list[int]) -> None: items.append(4) if __name__ == "__main__": items = [1, 2, 3] p = Process(target=add, args=(items,)) p.start() p.join() print(items)Anwenden · Aufgabe 7 von 7
Mini-Aufgabe, auf Ihrem eigenen Rechner. Schreiben Sie squares.py mit einer Funktion sum_of_squares(n) auf Modulebene, die die Summe von i * i für alle i unter n liefert. Berechnen Sie unter dem __main__-Guard mit einem Pool und einem einzigen map-Aufruf die Werte für 10, 100 und 1000 und geben Sie die Liste aus. Entfernen Sie dann den Guard und starten Sie erneut, um den RuntimeError zu sehen.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Primzahlen in vier Prozessen zählen
Primzahlen per Probedivision zu zählen ist CPU-lastig, also kann ein Pool aus vier Worker-Prozessen vier Kerne nutzen. Die Worker-Funktionen stehen in primes.py, main.py importiert sie: Funktionen auf Modulebene werden über ihren Namen gepickelt. main.py erzeugt den Pool und einen eigenen Process nur unter dem __main__-Guard. Speichern Sie beide Dateien in einem Ordner und starten Sie python main.py (unter macOS und Linux python3 main.py) auf Ihrem Rechner.
main.py
import multiprocessing as mp
from primes import check_limit, count_primes
LIMITS = [10_000, 20_000, 30_000, 40_000]
def main() -> None:
# Four worker processes; each call of count_primes runs in one of them.
with mp.Pool(processes=4) as pool:
counts = pool.map(count_primes, LIMITS)
for limit, count in zip(LIMITS, counts):
print(f"primes below {limit}: {count}")
# One process of our own: it exits with status 2 for a bad limit.
checker = mp.Process(target=check_limit, args=(1,))
checker.start()
checker.join()
print("checker exit code:", checker.exitcode)
if __name__ == "__main__":
main()
primes.py
import sys
def count_primes(limit: int) -> int:
"""Count the primes below limit, the slow way: CPU-bound work."""
count = 0
for n in range(2, limit):
if all(n % d for d in range(2, int(n**0.5) + 1)):
count += 1
return count
def check_limit(limit: int) -> None:
"""End the process with exit status 2 when there can be no prime below limit."""
if limit < 3:
sys.exit(2)
Ausführen mit
python main.pyAusgabe
primes below 10000: 1229
primes below 20000: 2262
primes below 30000: 3245
primes below 40000: 4203
checker exit code: 2- pool.map lieferte die Anzahlen in der Reihenfolge von LIMITS, egal welcher Worker zuerst fertig war.
- Der with-Block schließt den Pool: Seine Worker-Prozesse enden mit dem Block.
- sys.exit(2) im Kind beendete nur das Kind; der Elternprozess las den Status aus exitcode.
- Alles, was Prozesse startet, steht in main(), und das läuft nur unter dem Guard.
Übungen
Übung 1 von 2
Quadrate in einem Pool
check(n) liefert n * n und ob es in einem Kindprozess lief. Vervollständigen Sie squares_in_pool(numbers, workers) so, dass check für jede Zahl in einem Pool mit workers Prozessen läuft und die Liste der Ergebnisse in der richtigen Reihenfolge zurückkommt. Nutzen Sie eine with-Anweisung, damit der Pool geschlossen wird. Führen Sie die Tests auf Ihrem Rechner aus: Im Browser starten keine Prozesse.
Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Hinweise
Hinweis 1
with mp.Pool(processes=workers) as pool: erzeugt die Worker und schließt sie am Ende des Blocks.
Hinweis 2
pool.map(check, numbers) ruft check in den Workern einmal pro Zahl auf und liefert die Ergebnisse in der richtigen Reihenfolge.
Hinweis 3
Übergeben Sie die Funktion selbst, check, nicht check(n): Der Pool ruft sie für Sie auf.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import multiprocessing as mp
def check(n: int) -> tuple[int, bool]:
"""Square n, and report whether this call ran in a child process."""
return n * n, mp.parent_process() is not None
def squares_in_pool(numbers: list[int], workers: int = 2) -> list[tuple[int, bool]]:
"""Run check on every number in a pool of worker processes, in order."""
with mp.Pool(processes=workers) as pool:
return pool.map(check, numbers)
if __name__ == "__main__":
print(squares_in_pool([1, 2, 3]))
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import multiprocessing as mp
def check(n: int) -> tuple[int, bool]:
"""Square n, and report whether this call ran in a child process."""
return n * n, mp.parent_process() is not None
def squares_in_pool(numbers: list[int], workers: int = 2) -> list[tuple[int, bool]]:
"""Run check on every number in a pool of worker processes, in order."""
# This runs check in the main process. Use mp.Pool(processes=workers) and its map.
return [check(n) for n in numbers]
if __name__ == "__main__":
print(squares_in_pool([1, 2, 3]))
test_main.py
from main import squares_in_pool
def test_squares_in_order():
"""Die Quadrate kommen in der Reihenfolge der Zahlen zurück"""
got = [square for square, _ in squares_in_pool([3, 1, 2])]
assert got == [9, 1, 4], f"die Quadrate sind {got!r}, erwartet war [9, 1, 4]"
def test_in_child_processes():
"""Jeder Aufruf lief in einem Worker-Prozess, nicht im Hauptprozess"""
got = [in_child for _, in_child in squares_in_pool([1, 2, 3, 4])]
assert got == [True, True, True, True], f"lief in einem Kind: {got!r}; übergeben Sie check an pool.map"
def test_workers_argument():
"""workers=3 funktioniert auch"""
got = squares_in_pool([5, 6], workers=3)
assert got == [(25, True), (36, True)], f"squares_in_pool([5, 6], workers=3) lieferte {got!r}"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
Ein Worker, der sich picklen lässt
normalise_all entfernt Leerraum und schreibt Wörter in einem Pool klein, scheitert aber mit PicklingError: Der Worker clean ist innerhalb der Funktion definiert, und pickle verschickt Funktionen über ihren Namen. Verschieben Sie clean auf die oberste Ebene von main.py, ohne zu ändern, was es tut, damit pool.map es an die Worker schicken kann. Führen Sie die Tests auf Ihrem Rechner aus.
Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Hinweise
Hinweis 1
pickle speichert eine Funktion als Modul und qualifizierten Namen; normalise_all.<locals>.clean lässt sich von außen nicht nachschlagen.
Hinweis 2
Schneiden Sie den Block def clean aus normalise_all aus und fügen Sie ihn darüber ohne Einrückung ein.
Hinweis 3
normalise_all behält dann nur den Block with mp.Pool(...), der pool.map(clean, words) aufruft.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import multiprocessing as mp
def clean(word: str) -> str:
"""Strip and lower-case one word. At module level, so a worker can import it."""
return word.strip().lower()
def normalise_all(words: list[str], workers: int = 2) -> list[str]:
"""Strip and lower-case every word, in a pool of worker processes."""
with mp.Pool(processes=workers) as pool:
return pool.map(clean, words)
if __name__ == "__main__":
print(normalise_all([" Apple", "BANANA "]))
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import multiprocessing as mp
def normalise_all(words: list[str], workers: int = 2) -> list[str]:
"""Strip and lower-case every word, in a pool of worker processes."""
def clean(word: str) -> str:
return word.strip().lower()
with mp.Pool(processes=workers) as pool:
return pool.map(clean, words)
if __name__ == "__main__":
print(normalise_all([" Apple", "BANANA "]))
test_main.py
import pickle
import main
def test_normalise():
"""Wörter werden bereinigt und kleingeschrieben, in der richtigen Reihenfolge"""
got = main.normalise_all([" Apple", "BANANA ", "Cherry"])
assert got == ["apple", "banana", "cherry"], f"normalise_all lieferte {got!r}"
def test_clean_can_be_pickled():
"""clean ist eine Funktion auf Modulebene, die pickle an einen Worker schicken kann"""
clean = getattr(main, "clean", None)
assert clean is not None, "main.py hat keine Funktion clean auf Modulebene: verschieben Sie sie aus normalise_all heraus"
assert pickle.loads(pickle.dumps(clean)) is clean, "clean ließ sich nicht über den Namen picklen"
assert clean(" Kiwi ") == "kiwi", f"clean(' Kiwi ') lieferte {clean(' Kiwi ')!r}"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHäufige Fehler
Dem Pool ein lambda übergeben
from multiprocessing import Pool
if __name__ == "__main__":
with Pool(2) as pool:
print(pool.map(lambda x: x * 2, [1, 2, 3]))
Was Python ausgibt
_pickle.PicklingError: Can't pickle <function <lambda>Warum, und die Lösung
Der Pool pickelt die Funktion, um sie an die Worker zu schicken, und pickle speichert eine Funktion über Modul und Namen. Jedes lambda heißt <lambda>, der Worker könnte es also nicht finden. Schreiben Sie ein def auf oberster Ebene des Moduls, etwa def double(x): return x * 2, und übergeben Sie double.
Generatoren an Worker-Prozesse schicken
from multiprocessing import Pool
if __name__ == "__main__":
rows = [(x * x for x in range(3)), (x + 1 for x in range(3))]
with Pool(2) as pool:
print(pool.map(sum, rows))
Was Python ausgibt
TypeError: cannot pickle 'generator' objectWarum, und die Lösung
Ein Generator hält einen pausierten Frame, und der lässt sich nicht picklen. Alles, was Sie an einen Prozess übergeben, muss picklebar sein: Zahlen, Strings und Listen, Tupel und Dicts daraus. Bauen Sie stattdessen Listen, [x * x for x in range(3)], oder schicken Sie die Parameter (etwa 3) und lassen Sie den Worker den Generator selbst erzeugen.
map für eine Funktion mit zwei Parametern
from multiprocessing import Pool
if __name__ == "__main__":
with Pool(2) as pool:
print(pool.map(pow, [(2, 3), (3, 2)]))
Was Python ausgibt
TypeError: pow() missing required argument 'exp' (pos 2)Warum, und die Lösung
pool.map ruft die Funktion mit genau einem Argument pro Element auf, pow bekam also das Tupel (2, 3) als base. Die Exception des Workers wird im Elternprozess erneut ausgelöst. Nutzen Sie pool.starmap(pow, [(2, 3), (3, 2)]): Es entpackt jedes Tupel in die Argumente und liefert [8, 9].
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.