σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1,2 Tsd. Lernende
AnmeldenBeitreten ↗
Skills
AO
+1,2 Tsd. Lernende
Anmelden
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python-Skill›Modul 10 · Hochleistungs-Python-Entwicklung›Thema 10.9·~55 Min.Fortgeschritten

Threads und der GIL

Position in der SkillModul 10 von 11 · Thema 9 von 13
10.9 Threads und der GIL
—Lab-Punkte— / 100XP gesamt—Serie—

Threads: Nebenläufigkeit in einem Prozess

Ein Thread ist eine unabhängige Ausführungslinie innerhalb eines Programms, die ihren Speicher mit den anderen Threads teilt. Ein Tradingprogramm, das Kurse von zehn Handelsplätzen holen, ein Log schreiben und eine Oberfläche reaktionsfähig halten muss, nutzt Threads, damit das Warten auf einen die übrigen nicht aufhält. Pythons threading-Modul und bequemer concurrent.futures.ThreadPoolExecutor starten und verwalten sie. Weil Threads Variablen teilen, kommunizieren sie schnell, und Fehler sind gefährlich.

# Ten slow "network calls" in a thread pool wait at the same time, so the total is about one call, not ten
import time
from concurrent.futures import ThreadPoolExecutor

def fetch_quote(symbol):
    time.sleep(0.1)                                  # stands in for waiting on a venue
    return symbol, len(symbol) * 10.0

symbols = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOG", "NFLX", "ORCL", "INTC"]

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=10) as pool:
    quotes = list(pool.map(fetch_quote, symbols))
elapsed = time.perf_counter() - start

print(quotes[0], len(quotes))                        # ('AAPL', 40.0) 10
print(elapsed < 0.5)                                 # True  about 0.1 s in total, not 10 x 0.1 s

Die GIL: wartende und rechnende Threads

CPython hat eine Global Interpreter Lock (GIL): Nur ein Thread führt zu jedem Zeitpunkt Python-Bytecode aus. Ein Thread gibt sie ab, während er auf das Netz, eine Datei oder time.sleep wartet, daher überlappen Threads das Warten sehr gut: Zehn Threads, die Kurse holen, sind in etwa der Zeit eines einzigen fertig. Bei CPU-gebundenem Python-Code (eine reine Python-Schleife mit Arithmetik) wechseln sich die Threads aber ab, sodass vier Threads nicht schneller sind als einer, oft sogar etwas langsamer. Code, der außerhalb von Python läuft, etwa NumPy oder eine C-Erweiterung, die die GIL freigibt, kann in Threads parallel laufen. Für schwere reine Python-Berechnung nutzen Sie getrennte Prozesse (nächstes Thema) oder, wo verfügbar, den GIL-freien (free-threaded) Build von Python 3.13+.

# Splitting a list into chunks for workers: sizes differ by at most one, the first chunks get the extra items
def split_chunks(items, workers):
    size, extra = divmod(len(items), workers)
    chunks, start = [], 0
    for index in range(workers):
        stop = start + size + (1 if index < extra else 0)
        chunks.append(list(items[start:stop]))
        start = stop
    return chunks

chunks = split_chunks(list(range(7)), 3)
print(chunks)                                        # [[0, 1, 2], [3, 4], [5, 6]]
print([sum(chunk) for chunk in chunks])              # [3, 7, 11]   one partial sum per worker
print(sum(sum(chunk) for chunk in chunks))           # 21

Gemeinsame Daten, Race Conditions und Locks

Threads, die dieselbe Variable aktualisieren, können sie beschädigen, denn total += amount besteht aus drei Schritten (lesen, addieren, schreiben), und dazwischen kann ein Threadwechsel erfolgen: Zwei Threads lesen beide 100, addieren 5, schreiben 105, und ein Update geht verloren. Das ist eine Race Condition; sie tritt selten und unvorhersehbar auf und ist deshalb die schlimmste Art von Bug. Die Abhilfe ist ein Lock: with lock: lässt jeweils nur einen Thread den Block ausführen, und Lesen wie Schreiben müssen darin liegen. Noch besser ist, das Teilen zu vermeiden: Geben Sie jedem Worker seinen eigenen Teil und sein eigenes Ergebnis und fassen Sie die Ergebnisse am Ende zusammen, oder leiten Sie Arbeit und Ergebnisse über eine queue.Queue, die bereits threadsicher ist.

# A lock makes the read-modify-write a single step; the total comes out exact
import threading

totals = {"AAPL": 0}
lock = threading.Lock()

def safe_add(shared, key, amount, lock):
    with lock:
        shared[key] = shared.get(key, 0) + amount
        return shared[key]

def worker():
    for _ in range(10_000):
        safe_add(totals, "AAPL", 1, lock)

threads = [threading.Thread(target=worker) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(totals["AAPL"])                                # 40000  nothing lost: four threads x 10,000 additions
# A back-of-envelope estimate before writing any threaded code
import math

def estimate_time(n_tasks, seconds, workers, kind):
    if kind in ("io", "processes"):
        return math.ceil(n_tasks / workers) * seconds
    return n_tasks * seconds                         # "cpu_threads": the GIL makes the threads take turns

print(estimate_time(40, 0.5, 8, "io"))               # 2.5    forty slow requests, eight at a time
print(estimate_time(40, 0.5, 8, "cpu_threads"))      # 20.0   forty CPU-bound tasks in threads: no gain
print(estimate_time(40, 0.5, 8, "processes"))        # 2.5    separate processes use separate cores
Threads sind öfter das falsche Werkzeug, als man denktThreads passen zu I/O: viele langsame, unabhängige Wartezeiten. Für viele Verbindungen in einem Prozess skaliert asyncio (weiter vorn in diesem Lernpfad) meist besser und hat keine Races zwischen awaits. Für Arithmetik auf Arrays vektorisieren Sie mit NumPy. Für CPU-gebundenes Python nutzen Sie Prozesse. Teilen Sie doch Daten zwischen Threads, halten Sie den gesperrten Abschnitt kurz, rufen Sie nie langsamen Code auf, während Sie ein Lock halten, und nehmen Sie mehrere Locks überall in derselben Reihenfolge, sonst können zwei Threads ewig aufeinander warten (ein Deadlock).
  • Threads teilen Speicher und überlappen Wartezeiten gut (Netz, Dateien); ein ThreadPoolExecutor kann viele Handelsplätze gleichzeitig abfragen.
  • Die GIL lässt nur einen Thread gleichzeitig Python-Bytecode ausführen: kein Speed-up für CPU-gebundenes reines Python, aber NumPy und C-Erweiterungen können sie freigeben.
  • Schützen Sie gemeinsame Updates mit with lock: um Lesen und Schreiben, teilen Sie lieber nichts (Teile, Queues) und achten Sie auf Deadlocks.

Als Nächstes: Multiprocessing, mehrere Prozesse nutzen, um jeden CPU-Kern arbeiten zu lassen.

Lab: Was bewertet wird

  • Mit split_chunks die Arbeit verteilen25 Pkt.
  • Mit run_in_chunks eine Funktion auf die Teile anwenden25 Pkt.
  • Gemeinsame Daten mit safe_add schützen25 Pkt.
  • Mit estimate_time wissen, was Threads bringen25 Pkt.
Explorer · lab-125-1
▾ lab-125-1
…
bash · im Browser
learner@quantcraft:~/lab-125-1$
Python noch nicht geladenPythonLn 1, Col 1Spaces: 4UTF-8

Dieses Thema zurücksetzen?

Alles in lab-125-1 wird auf die Startdateien zurückgesetzt und das Terminal geleert. Ihre aktuelle Lösung geht verloren – das lässt sich nicht rückgängig machen.

Wird zurückgesetzt
  • main.py → Startcode
  • Terminal-Verlauf und Testausgabe
  • Nicht eingereichte Änderungen
Bleibt erhalten
  • Beste Lab-Punktzahl
  • Ihre XP und Ihre Serie
  • Vorlesungsfortschritt und Notizen

Melde dich an, um deinen Code auszuführen.

Alle Lektionen kannst du kostenlos lesen. Erstelle ein Konto oder melde dich an, um Code auszuführen, Tests zu starten und dieses Lab abzugeben.

  • IDE im Browser mit Bash-Terminal — nichts zu installieren
  • Sofortige Bewertung, XP und dein Platz auf der Skill
Konto erstellenAnmelden
ODER
Weiter mit GitHub