Zum Inhalt springen
aviral gupta

// A3.6 · ca. 50 Min. · Vertiefung

Das passende Nebenläufigkeitsmodell wählen

Nach dieser Lektion entscheiden Sie anhand der Art der Arbeit und anhand von Messungen zwischen Threads, Prozessen und asyncio und haben einen Datei-Hasher in allen drei Modellen gebaut.

Lektion 6 von 6 in A3 Nebenläufigkeit

Ende des Moduls

Danach können Sie

  • I/O-lastige von CPU-lastiger Arbeit unterscheiden und das jeweils passende Modell nennen
  • Dieselbe Funktion zwischen Thread-Pools, Prozess-Pools und asyncio verschieben
  • Eine nebenläufige Version mit time.perf_counter gegen die sequenzielle messen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der letzten Lektion: Eine Coroutine muss eine langsame, blockierende Funktion aufrufen, ohne die Event-Loop einzufrieren. Was schreibt sie?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. Auf einem Standard-Build mit GIL führen Sie 8 Downloads, die je 0,05 s warten, und 4 Zählschleifen in reinem Python jeweils in einem ThreadPoolExecutor aus. Was wird deutlich schneller als nacheinander?

  3. Üben · Aufgabe 3 von 7

    Setzen Sie die Methode ein, die den Digest als Zeichenkette aus Hexadezimalziffern liefert.

    import hashlib
    import io
    
    f = io.BytesIO(b"hello")
    print(hashlib.file_digest(f, "sha256").____())
    hashlib.file_digest(f, "sha256").()
  4. Üben · Aufgabe 4 von 7

    Was gibt das aus?

    import hashlib
    
    data = b"x" * 10_000_000
    print(len(hashlib.sha256(data).hexdigest()), hashlib.sha256(data).digest_size)
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jeder Aufgabe das Modell zu, das auf einem Standard-Build meist am besten passt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. 100 Aufrufe von abs, einmal in einer List Comprehension und einmal in einem Prozess-Pool. Was gibt das aus?

    import time
    from concurrent.futures import ProcessPoolExecutor
    
    
    def main():
        start = time.perf_counter()
        sequential = [abs(n) for n in range(-50, 50)]
        sequential_time = time.perf_counter() - start
    
        start = time.perf_counter()
        with ProcessPoolExecutor(max_workers=2) as pool:
            pooled = list(pool.map(abs, range(-50, 50)))
        pooled_time = time.perf_counter() - start
    
        print(sequential == pooled, pooled_time > sequential_time)
    
    
    if __name__ == "__main__":
        main()
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe, auf Ihrem eigenen Rechner. Schreiben Sie in einem temporären Ordner 8 Dateien mit je 2 MB Zufallsbytes (os.urandom). Hashen Sie sie mit hashlib.file_digest einmal in einer List Comprehension und einmal mit ThreadPoolExecutor.map, messen Sie beides mit time.perf_counter und geben Sie die beiden Zeiten aus und ob die Digests gleich sind. Starten Sie es ein paar Mal: Was ist auf Ihrem Rechner schneller?

    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

Threads beim Warten und beim Rechnen messen

work.py enthält zwei Funktionen auf Modulebene: wait schläft wie ein I/O-Aufruf, und count rechnet in einer Schleife in reinem Python. main.py misst die Wartezeiten nacheinander und in einem Thread-Pool, misst, wie viele Kerne das Zählen in einem Thread-Pool beschäftigt, und führt es zusätzlich in einem Prozess-Pool aus. Es gibt Vergleiche statt roher Zeiten aus, weil sich Zeiten von Lauf zu Lauf ändern. Speichern Sie beide Dateien und starten Sie python main.py (unter macOS und Linux python3 main.py) auf Ihrem Rechner; geben Sie die Zeiten zusätzlich aus, um Ihre eigenen Werte zu sehen.

main.py

import sys
import time
from collections.abc import Callable
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from typing import Any

from work import count, wait


def timed(run: Callable[[], list[Any]]) -> tuple[list[Any], float]:
    """Run once and return (results, seconds); perf_counter is made for short durations."""
    start = time.perf_counter()
    results = run()
    return results, time.perf_counter() - start


def main() -> None:
    waits = [0.05] * 8
    _, sequential = timed(lambda: list(map(wait, waits)))
    with ThreadPoolExecutor(max_workers=8) as pool:
        _, threaded = timed(lambda: list(pool.map(wait, waits)))
    print("I/O-bound, 8 waits of 0.05 s:")
    print("  threads at least 3x faster than sequential:", sequential / threaded >= 3)

    sizes = [1_000_000] * 4
    seq_totals = list(map(count, sizes))
    with ThreadPoolExecutor(max_workers=4) as pool:
        cpu_start = time.process_time()  # CPU seconds used by all threads of this process
        thread_totals, wall = timed(lambda: list(pool.map(count, sizes)))
        cores_busy = (time.process_time() - cpu_start) / wall
    with ProcessPoolExecutor(max_workers=4) as pool:
        process_totals = list(pool.map(count, sizes))
    print(f"CPU-bound, 4 counts in pure Python (GIL enabled: {sys._is_gil_enabled()}):")
    print("  threads kept more than 1.5 cores busy:", cores_busy > 1.5)
    print("  all three gave the same totals:", seq_totals == thread_totals == process_totals)


if __name__ == "__main__":
    main()

work.py

import time


def wait(seconds: float) -> float:
    """I/O-bound stand-in: the thread just waits, like a download or a disk read."""
    time.sleep(seconds)
    return seconds


def count(n: int) -> int:
    """CPU-bound: pure Python arithmetic, holding the GIL the whole time."""
    total = 0
    for i in range(n):
        total += i % 7
    return total

Ausführen mit

python main.py

Ausgabe

I/O-bound, 8 waits of 0.05 s:
  threads at least 3x faster than sequential: True
CPU-bound, 4 counts in pure Python (GIL enabled: True):
  threads kept more than 1.5 cores busy: False
  all three gave the same totals: True
  • Acht Wartezeiten von 0,05 s überlappten in acht Threads: etwa 0,05 s statt 0,4 s.
  • Die Zählschleifen hielten den GIL, also beschäftigten vier Threads etwa einen Kern: time.process_time() stieg um etwa eine CPU-Sekunde pro Sekunde Uhrzeit.
  • Der Prozess-Pool führte dieselbe Funktion count aus, importiert aus work.py, und lieferte dieselben Summen; auf mehreren Kernen ist er derjenige, der schneller sein kann.
  • Auf einem Free-Threaded-Build würde GIL enabled False ausgeben, und die Zeile zu den Threads könnte sich ändern: Messen Sie auf dem Build, den Sie einsetzen.

Übungen

Übung 1 von 4

Projektschritt 1: eine Datei hashen, dann alle

Modulprojekt: ein Datei-Hasher in drei Modellen. Beginnen Sie mit der einfachen Version. Schreiben Sie hash_file(path), das die Datei im Binärmodus öffnet und den SHA-256-Hex-Digest von hashlib.file_digest liefert. Schreiben Sie dann hash_sequential(paths), das ein Dict von jedem Pfad zu seinem Digest liefert, in der Reihenfolge von paths. 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

    with open(path, "rb") as f: öffnet die Datei im Binärmodus, den file_digest verlangt.

  2. Hinweis 2

    hashlib.file_digest(f, "sha256") liefert ein Hash-Objekt; .hexdigest() liefert die Zeichenkette.

  3. Hinweis 3

    hash_sequential ist eine einzige Dict-Comprehension: {path: hash_file(path) for path in paths}.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import hashlib


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}
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 hashlib


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    # Open the file in binary mode and pass it to hashlib.file_digest with "sha256".
    return ""


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {}

test_main.py

import hashlib
import os
import tempfile

from main import hash_file, hash_sequential


def make_files(folder, contents):
    paths = []
    for number, data in enumerate(contents):
        path = os.path.join(folder, f"file{number}.bin")
        with open(path, "wb") as f:
            f.write(data)
        paths.append(path)
    return paths


def test_hash_file():
    """hash_file liefert den SHA-256-Hex-Digest der Bytes der Datei"""
    with tempfile.TemporaryDirectory() as folder:
        [path] = make_files(folder, [b"hello"])
        got = hash_file(path)
    assert got == hashlib.sha256(b"hello").hexdigest(), f"hash_file lieferte {got!r}"


def test_large_file():
    """Eine Datei von 1 MB ergibt den Digest ihrer Bytes"""
    data = bytes(range(256)) * 4096
    with tempfile.TemporaryDirectory() as folder:
        [path] = make_files(folder, [data])
        got = hash_file(path)
    assert got == hashlib.sha256(data).hexdigest(), f"hash_file lieferte {got!r}"


def test_hash_sequential():
    """hash_sequential bildet jeden Pfad auf seinen Digest ab, in der Reihenfolge von paths"""
    with tempfile.TemporaryDirectory() as folder:
        paths = make_files(folder, [b"b", b"a"])
        got = hash_sequential(paths)
    expected = [(paths[0], hashlib.sha256(b"b").hexdigest()), (paths[1], hashlib.sha256(b"a").hexdigest())]
    assert list(got.items()) == expected, f"hash_sequential 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 4

Projektschritt 2: Threads

hash_threads(paths, workers) hasht noch im Haupt-Thread. Hashen Sie die Dateien in einem ThreadPoolExecutor mit workers Threads, rufen Sie für jeden Pfad hash_file auf und liefern Sie dasselbe Dict wie hash_sequential. Lesen wartet auf die Festplatte, und hashlib gibt bei großen Daten den GIL frei, also können Threads hier helfen. 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

    Öffnen Sie den Pool in einem with-Block: with ThreadPoolExecutor(max_workers=workers) as pool:.

  2. Hinweis 2

    pool.map(hash_file, paths) liefert die Digests in der Reihenfolge von paths.

  3. Hinweis 3

    dict(zip(paths, digests)) verbindet jeden Pfad mit seinem Digest.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import hashlib
from concurrent.futures import ThreadPoolExecutor


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}


def hash_threads(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a thread pool: file reads wait on the disk, and hashlib releases the GIL."""
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))
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 hashlib
from concurrent.futures import ThreadPoolExecutor


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}


def hash_threads(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a thread pool: file reads wait on the disk, and hashlib releases the GIL."""
    # Still one after another in the main thread. Use a ThreadPoolExecutor.
    return hash_sequential(paths)

test_main.py

import threading
import tempfile
import os

import main


def make_files(folder, contents):
    paths = []
    for number, data in enumerate(contents):
        path = os.path.join(folder, f"file{number}.bin")
        with open(path, "wb") as f:
            f.write(data)
        paths.append(path)
    return paths


def test_same_digests():
    """hash_threads liefert dasselbe Dict wie hash_sequential"""
    with tempfile.TemporaryDirectory() as folder:
        paths = make_files(folder, [b"one", b"two", b"three"])
        got = main.hash_threads(paths, workers=2)
        expected = main.hash_sequential(paths)
    assert list(got.items()) == list(expected.items()), f"hash_threads lieferte {got!r}"


def test_runs_in_pool_threads():
    """hash_threads ruft hash_file in Worker-Threads auf, nicht im Haupt-Thread"""
    seen = []

    def spy(path):
        seen.append(threading.current_thread().name)
        return path.upper()

    real = main.hash_file
    main.hash_file = spy
    try:
        got = main.hash_threads(["a", "b"], workers=2)
    finally:
        main.hash_file = real
    assert got == {"a": "A", "b": "B"}, f"hash_threads lieferte {got!r}"
    assert "MainThread" not in seen, f"hash_file lief in {seen!r}: nutzen Sie einen ThreadPoolExecutor"

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 3 von 4

Projektschritt 3: Prozesse

hash_processes(paths, workers) nutzt noch Threads. Nutzen Sie stattdessen den oben importierten ProcessPoolExecutor mit max_workers=workers und liefern Sie dasselbe Dict. hash_file ist eine Funktion auf Modulebene, also können die Worker sie entpicklen, und der __main__-Guard unten verhindert, dass sie das Skript erneut ausführen. 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

    Der Rumpf sieht aus wie hash_threads, mit einem geänderten Namen.

  2. Hinweis 2

    with ProcessPoolExecutor(max_workers=workers) as pool: und dann dict(zip(paths, pool.map(hash_file, paths))).

  3. Hinweis 3

    Übergeben Sie pool.map kein lambda: Ein Prozess-Pool kann nur Funktionen auf Modulebene verschicken.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import hashlib
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}


def hash_threads(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a thread pool: file reads wait on the disk, and hashlib releases the GIL."""
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


def hash_processes(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a process pool; hash_file is a module-level function, so it pickles."""
    with ProcessPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


if __name__ == "__main__":
    import sys

    print(hash_processes(sys.argv[1:]))
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 hashlib
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}


def hash_threads(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a thread pool: file reads wait on the disk, and hashlib releases the GIL."""
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


def hash_processes(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a process pool; hash_file is a module-level function, so it pickles."""
    # Still threads. Use the ProcessPoolExecutor imported above.
    return hash_threads(paths, workers)


if __name__ == "__main__":
    import sys

    print(hash_processes(sys.argv[1:]))

test_main.py

import tempfile
import os

import main


def make_files(folder, contents):
    paths = []
    for number, data in enumerate(contents):
        path = os.path.join(folder, f"file{number}.bin")
        with open(path, "wb") as f:
            f.write(data)
        paths.append(path)
    return paths


def test_uses_process_pool():
    """hash_processes hasht in einem ProcessPoolExecutor und liefert dasselbe Dict wie hash_sequential"""
    used = []
    real = main.ProcessPoolExecutor

    class Spy(real):
        def __init__(self, *args, **kwargs):
            used.append(kwargs.get("max_workers", args[0] if args else None))
            super().__init__(*args, **kwargs)

    main.ProcessPoolExecutor = Spy
    try:
        with tempfile.TemporaryDirectory() as folder:
            paths = make_files(folder, [b"one", b"two", b"three"])
            got = main.hash_processes(paths, workers=2)
            expected = main.hash_sequential(paths)
    finally:
        main.ProcessPoolExecutor = real
    assert used == [2], f"ProcessPoolExecutor wurde mit {used!r} erzeugt: erzeugen Sie einen mit max_workers=workers"
    assert list(got.items()) == list(expected.items()), f"hash_processes 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 4 von 4

Projektschritt 4: asyncio

Das letzte Modell: hash_async(paths) ist eine Coroutine, ruft hash_file aber direkt auf, und das blockiert die Event-Loop. Führen Sie jedes hash_file(path) mit asyncio.to_thread aus, als Task in einer asyncio.TaskGroup, und liefern Sie das Dict in der Reihenfolge von paths. Messen Sie danach alle vier Funktionen mit Ihren eigenen Dateien mit time.perf_counter und vergleichen Sie. 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

    asyncio.to_thread(hash_file, path) ist eine Coroutine, die hash_file in einem eigenen Thread ausführt.

  2. Hinweis 2

    Halten Sie innerhalb von async with asyncio.TaskGroup() as tg: {path: tg.create_task(asyncio.to_thread(hash_file, path)) for path in paths}.

  3. Hinweis 3

    Bauen Sie nach dem Block {path: task.result() for path, task in tasks.items()}.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import asyncio
import hashlib
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}


def hash_threads(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a thread pool: file reads wait on the disk, and hashlib releases the GIL."""
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


def hash_processes(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a process pool; hash_file is a module-level function, so it pickles."""
    with ProcessPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


async def hash_async(paths: list[str]) -> dict[str, str]:
    """Hash the files from asyncio: each blocking hash_file call runs in a thread via to_thread."""
    async with asyncio.TaskGroup() as tg:
        tasks = {path: tg.create_task(asyncio.to_thread(hash_file, path)) for path in paths}
    return {path: task.result() for path, task in tasks.items()}


if __name__ == "__main__":
    import sys

    print(hash_processes(sys.argv[1:]))
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 asyncio
import hashlib
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def hash_file(path: str) -> str:
    """Return the SHA-256 hex digest of the file at path."""
    with open(path, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def hash_sequential(paths: list[str]) -> dict[str, str]:
    """Hash the files one after another: {path: digest}, in the order of paths."""
    return {path: hash_file(path) for path in paths}


def hash_threads(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a thread pool: file reads wait on the disk, and hashlib releases the GIL."""
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


def hash_processes(paths: list[str], workers: int = 4) -> dict[str, str]:
    """Hash the files in a process pool; hash_file is a module-level function, so it pickles."""
    with ProcessPoolExecutor(max_workers=workers) as pool:
        return dict(zip(paths, pool.map(hash_file, paths)))


async def hash_async(paths: list[str]) -> dict[str, str]:
    """Hash the files from asyncio: each blocking hash_file call runs in a thread via to_thread."""
    # Calling hash_file directly blocks the event loop.
    # Run each call with asyncio.to_thread, as a task in an asyncio.TaskGroup.
    return hash_sequential(paths)


if __name__ == "__main__":
    import sys

    print(hash_processes(sys.argv[1:]))

test_main.py

import asyncio
import tempfile
import threading
import os

import main


def make_files(folder, contents):
    paths = []
    for number, data in enumerate(contents):
        path = os.path.join(folder, f"file{number}.bin")
        with open(path, "wb") as f:
            f.write(data)
        paths.append(path)
    return paths


def test_same_digests():
    """hash_async liefert dasselbe Dict wie hash_sequential, in der Reihenfolge von paths"""
    with tempfile.TemporaryDirectory() as folder:
        paths = make_files(folder, [b"one", b"two", b"three"])
        got = asyncio.run(main.hash_async(paths))
        expected = main.hash_sequential(paths)
    assert list(got.items()) == list(expected.items()), f"hash_async lieferte {got!r}"


def test_event_loop_not_blocked():
    """hash_async führt hash_file in Worker-Threads aus, abseits der Event-Loop"""
    seen = []

    def spy(path):
        seen.append(threading.current_thread().name)
        return path.upper()

    real = main.hash_file
    main.hash_file = spy
    try:
        got = asyncio.run(main.hash_async(["a", "b"]))
    finally:
        main.hash_file = real
    assert got == {"a": "A", "b": "B"}, f"hash_async lieferte {got!r}"
    assert "MainThread" not in seen, f"hash_file lief in {seen!r}: nutzen Sie asyncio.to_thread"

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

Ein lambda in einem Prozess-Pool

import hashlib
from concurrent.futures import ProcessPoolExecutor

texts = ["alpha", "beta"]

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=2) as pool:
        digests = list(pool.map(lambda text: hashlib.sha256(text.encode()).hexdigest(), texts))
    print(digests)

Was Python ausgibt

_pickle.PicklingError: Can't pickle <function <lambda>

Warum, und die Lösung

Die Thread-Version funktionierte mit einem lambda, aber ein Prozess-Pool pickelt die Funktion über ihren Namen, und ein lambda hat keinen, den ein Worker nachschlagen könnte. Definieren Sie die Arbeit als Funktion auf Modulebene, etwa def hash_text(text: str) -> str:, und übergeben Sie diese an pool.map.

Die Funktion in asyncio.to_thread aufrufen

import asyncio
import hashlib


def hash_text(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()


async def main() -> None:
    digest = await asyncio.to_thread(hash_text("alpha"))
    print(digest)


asyncio.run(main())

Was Python ausgibt

TypeError: 'str' object is not callable

Warum, und die Lösung

hash_text("alpha") läuft sofort, im Thread der Event-Loop, und to_thread bekommt sein Ergebnis, eine Zeichenkette. Übergeben Sie Funktion und Argumente getrennt: await asyncio.to_thread(hash_text, "alpha"). mypy meldet diese Zeile, bevor Sie sie ausführen.

Eine im Textmodus geöffnete Datei hashen

import hashlib

with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("hello")

with open("notes.txt") as f:
    print(hashlib.file_digest(f, "sha256").hexdigest())

Was Python ausgibt

ValueError: '<_io.TextIOWrapper name='notes.txt' mode='r' encoding='utf-8'>' is not a file-like object in binary reading mode.

Warum, und die Lösung

file_digest hasht Bytes, also muss die Datei zum Lesen im Binärmodus geöffnet sein: open("notes.txt", "rb"). Der Textmodus würde außerdem dekodieren und Zeilenenden ändern, was den Digest verändert.

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

Fragen Sie zuerst, worauf das Programm wartet

I/O-lastige Arbeit verbringt ihre Zeit mit Warten: auf das Netzwerk, die Festplatte, eine Datenbank. Threads und asyncio überlappen die Wartezeiten günstig, denn ein wartender Thread gibt den GIL frei und eine wartende Coroutine pausiert. CPU-lastige Arbeit verbringt ihre Zeit mit Rechnen. Reiner Python-Code hält auf einem Standard-Build den GIL, also wechseln sich Threads ab; Prozesse oder ein InterpreterPoolExecutor geben jedem Worker einen eigenen GIL. C-Code, der den GIL freigibt, ist die Ausnahme: hashlib gibt ihn frei, während es mehr als 2047 Bytes auf einmal hasht, also skaliert das Hashen großer Dateien in Threads.

Dieselbe Funktion, drei Modelle

Halten Sie die Arbeit in einer einfachen Funktion auf Modulebene, etwa hash_file(path). Dann ist jedes Modell eine dünne Hülle: ThreadPoolExecutor().map(hash_file, paths), ProcessPoolExecutor().map(hash_file, paths) hinter einem __main__-Guard oder asyncio.to_thread(hash_file, path) als Tasks in einer TaskGroup. to_thread hält einen blockierenden Aufruf von der Event-Loop fern; es ist für I/O-lastige Funktionen gedacht. Auf dem Free-Threaded-Build, auf dem sys._is_gil_enabled() False liefert, können Threads auch CPU-lastigen Python-Code parallel ausführen.

Erst messen, dann entscheiden

Messen Sie jede Version mit derselben realistischen Eingabe mit time.perf_counter(): Nehmen Sie die Differenz zweier Aufrufe, denn nur Differenzen sind aussagekräftig. Messen Sie mehr als einmal, denn der erste Lauf bezahlt für kalte Caches und startende Worker. Prozesse kosten Startzeit und das Picklen jedes Arguments und Ergebnisses, also verlieren sie bei vielen winzigen Aufgaben und gewinnen bei wenigen großen, CPU-lastigen. Ist die nebenläufige Version nicht deutlich schneller, behalten Sie den einfacheren sequenziellen Code.

Quellen

Zuletzt geprüft am 29. September 2026