Zum Inhalt springen
aviral gupta

// 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.

Lektion 2 von 6 in A3 Nebenläufigkeit

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
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der letzten Lektion: Welche Aussagen über Threads im Standard-Build von CPython stimmen? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. 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)
  3. Ü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__ == :
  4. Ü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)
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Baustein von multiprocessing zu, was er Ihnen liefert.

  6. 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)
  7. 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.py

Ausgabe

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
  1. Hinweis 1

    with mp.Pool(processes=workers) as pool: erzeugt die Worker und schließt sie am Ende des Blocks.

  2. Hinweis 2

    pool.map(check, numbers) ruft check in den Workern einmal pro Zahl auf und liefert die Ergebnisse in der richtigen Reihenfolge.

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.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
  1. Hinweis 1

    pickle speichert eine Funktion als Modul und qualifizierten Namen; normalise_all.<locals>.clean lässt sich von außen nicht nachschlagen.

  2. Hinweis 2

    Schneiden Sie den Block def clean aus normalise_all aus und fügen Sie ihn darüber ohne Einrückung ein.

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Hä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' object

Warum, 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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Prozess pro Aufgabe, ein GIL pro Prozess

multiprocessing umgeht den GIL, indem es Prozesse statt Threads nutzt. Jedes Kind ist ein eigener Python-Interpreter mit eigenem Speicher und eigenem GIL, also läuft CPU-lastiger Code wirklich auf mehreren Kernen. Process(target=f, args=(x,)) funktioniert wie Thread: start(), join() und danach exitcode (0 bei Erfolg, 1 nach einer unbehandelten Exception). Pool(processes=4) hält Worker-Prozesse bereit; pool.map(f, items) ist ein paralleles map(), das wartet, bis alle Ergebnisse da sind, und sie in der Reihenfolge von items liefert. starmap entpackt Tupel in mehrere Argumente.

Der __main__-Guard

Die Standard-Startmethode ist spawn unter Windows und macOS und seit 3.14 forkserver auf den übrigen POSIX-Systemen. Beide starten einen frischen Interpreter, der Ihr Hauptmodul unter dem Namen __mp_main__ importiert, um die Zielfunktion zu finden. Code auf oberster Ebene läuft also in jedem Kind erneut. Erzeugen Sie Prozesse und Pools deshalb nur unter if __name__ == "__main__":. Ohne Guard würde das Kind selbst Prozesse starten, und multiprocessing stoppt das mit einem RuntimeError. Definitionen, Konstanten und Importe auf oberster Ebene sind unproblematisch.

Alles reist per pickle

Die Zielfunktion, ihre Argumente und die Ergebnisse werden gepickelt und zwischen den Prozessen verschickt. Funktionen werden über ihren qualifizierten Namen gepickelt, nicht über ihren Code. Ein Worker muss also ein def auf oberster Ebene eines Moduls sein: Ein lambda oder eine verschachtelte Funktion lässt sich nicht picklen. Generatoren, Locks und offene Dateien auch nicht. Weil das Kind eine Kopie bekommt, bleibt eine Liste oder ein Dict, das es ändert, im Elternprozess unverändert. Ergebnisse kommen als Rückgabewerte von pool.map zurück oder über eine multiprocessing.Queue.

Quellen

Zuletzt geprüft am 29. September 2026