overpass/TASK.md

2.6 KiB

Task 16 — Lock und @timer-Decorator

Rückblick Task 15: Concurrency

Ihr habt ThreadPoolExecutor und as_completed eingeführt. Die wichtigsten Punkte:

  • I/O-bound vs. CPU-bound: Overpass-Requests sind I/O-bound — die CPU wartet auf die Netzwerkantwort. Threads sind dafür ideal, weil Python während des Wartens (I/O) den GIL freigibt und andere Threads laufen lässt. Bei CPU-bound Tasks (z.B. Bildverarbeitung, ML-Training) hilft Threading nicht — dort braucht man multiprocessing.
  • executor.map() vs. as_completed(): map() ist einfacher, liefert Ergebnisse aber in der Reihenfolge der Inputs — auch wenn spätere Futures früher fertig sind. as_completed() liefert Ergebnisse sobald sie fertig sind, was bei unterschiedlichen Antwortzeiten effizienter ist und pro Future individuelles Error-Handling erlaubt.
  • all_pois.extend() aus mehreren Threads: In Python ist list.extend() durch den GIL (Global Interpreter Lock) de facto atomar für einfache Operationen — ein echter Race Condition-Crash ist unwahrscheinlich. Aber: die Reihenfolge der Ergebnisse ist nicht deterministisch, und bei komplexeren Operationen (read-modify-write) wäre ein Lock nötig.

Aufgabe

Zwei Erweiterungen stehen an — eine zur Illustration von Thread-Safety, eine zur Laufzeitmessung.

Teil A — FetchMode.CONCURRENT_LOCKED:

Der bisherige CONCURRENT-Modus sammelt Ergebnisse ohne explizite Synchronisation. Füge einen dritten Modus hinzu, der zeigt, wie man all_pois.extend() mit einem Lock absichert.

  1. Ergänze FetchMode um CONCURRENT_LOCKED = "concurrent_locked".
  2. Implementiere den neuen Modus in fetch_and_store() analog zu CONCURRENT, aber mit einem threading.Lock:
   with lock:
       all_pois.extend(future.result())
  1. Ergänze config.yaml — setze fetch_mode: concurrent_locked.

Teil B — @timer-Decorator:

  1. Lege eine neue Datei utils.py an.

  2. Schreibe darin einen @timer-Decorator, der die Laufzeit einer Funktion misst und per logger.info() ausgibt.

    def timer(func):
     @wraps(func)
     def wrapper(*args, **kwargs):
         start   = time.perf_counter()
         result  = func(*args, **kwargs)
         elapsed = time.perf_counter() - start
         logger.info(f"[timer] {func.__name__}{elapsed:.2f}s")
         return result
     return wrapper
    
  3. Dekoriere main() in main.py mit @timer.

Fragen zum Nachdenken:

  • list.extend() ist in CPython durch den GIL geschützt — warum empfiehlt es sich trotzdem, einen Lock zu verwenden?