σμ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 AcademyParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Parcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Python›Module 10 · Ingénierie Python haute performance›Sujet 10.9·~55 minAvancé

Threads et GIL

Position dans le parcoursModule 10 sur 11 · Sujet 9 sur 13
10.9 Threads et GIL
—Score du lab— / 100XP total—Série—

Threads : de la concurrence dans un seul processus

Un thread est une ligne d'exécution indépendante dans un même programme, qui partage sa mémoire avec les autres threads. Un programme de trading qui doit récupérer des cotations de dix places, écrire un journal et garder une interface réactive utilise des threads pour que l'attente de l'un n'arrête pas les autres. Le module threading de Python et, plus commodément, concurrent.futures.ThreadPoolExecutor les lancent et les gèrent. Comme les threads partagent des variables, ils communiquent vite et il est dangereux de se tromper.

# 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

Le GIL : des threads qui attendent et des threads qui calculent

CPython a un verrou global de l'interpréteur (GIL) : un seul thread exécute du bytecode Python à un instant donné. Un thread le cède pendant qu'il attend le réseau, un fichier ou time.sleep, si bien que les threads se chevauchent très bien dans l'attente : dix threads récupérant des cotations finissent en à peu près le temps d'un seul. Mais pour du code Python lié au processeur (une boucle Python pure faisant de l'arithmétique), les threads se relaient, donc quatre threads ne vont pas plus vite qu'un, et souvent un peu moins vite. Le code qui s'exécute hors de Python, comme NumPy ou une extension C qui relâche le GIL, peut tourner en parallèle dans des threads. Pour un calcul Python pur lourd, utilisez des processus séparés (sujet suivant), ou la version sans GIL (free-threaded) de Python 3.13+, quand elle est disponible.

# 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

Données partagées, conditions de course et verrous

Des threads qui mettent à jour la même variable peuvent la corrompre, car total += amount fait trois étapes (lire, additionner, écrire) et un changement de thread peut survenir entre elles : deux threads lisent tous deux 100, ajoutent 5, écrivent 105, et une mise à jour est perdue. C'est une condition de course, qui apparaît rarement et de façon imprévisible, ce qui en fait le pire type de bug. Le remède est un verrou : with lock: laisse un seul thread à la fois exécuter le bloc, et la lecture comme l'écriture doivent y être. Mieux encore, évitez le partage : donnez à chaque travailleur son propre morceau et son propre résultat, puis combinez les résultats à la fin, ou faites passer travail et résultats par une queue.Queue, déjà sûre pour les threads.

# 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
Les threads sont plus souvent le mauvais outil qu'on ne croitLes threads conviennent à l'E/S : de nombreuses attentes lentes et indépendantes. Pour de nombreuses connexions dans un processus, asyncio (vu plus tôt dans ce parcours) passe en général mieux à l'échelle et n'a pas de courses entre les await. Pour l'arithmétique sur tableaux, vectorisez avec NumPy. Pour du Python lié au processeur, utilisez des processus. Si vous partagez des données entre threads, gardez la section verrouillée courte, n'appelez jamais de code lent en tenant un verrou, et prenez plusieurs verrous dans le même ordre partout, sinon deux threads peuvent s'attendre pour toujours (un interblocage).
  • Les threads partagent la mémoire et se chevauchent bien dans l'attente (réseau, fichiers) ; un ThreadPoolExecutor peut interroger de nombreuses places à la fois.
  • Le GIL ne laisse qu'un thread exécuter du bytecode Python à la fois : pas d'accélération pour du Python pur lié au processeur, mais NumPy et les extensions C peuvent le relâcher.
  • Protégez les mises à jour partagées par with lock: autour de la lecture et de l'écriture, préférez ne pas partager (morceaux, files) et surveillez les interblocages.

À suivre : le multiprocessing, utiliser plusieurs processus pour faire travailler chaque cœur du processeur.

Lab : ce qui est noté

  • Répartir le travail avec split_chunks25 pts
  • Appliquer une fonction aux morceaux avec run_in_chunks25 pts
  • Protéger des données partagées avec safe_add25 pts
  • Savoir ce que les threads apportent avec estimate_time25 pts
Explorateur · lab-125-1
▾ lab-125-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-125-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-125-1 revient aux fichiers de départ et le terminal est vidé. Votre solution actuelle sera perdue : cette action est irréversible.

Sera réinitialisé
  • main.py → code de départ
  • Historique du terminal et sortie des tests
  • Modifications non soumises
Sera conservé
  • Meilleur score du lab
  • Votre XP et votre série
  • Progression du cours et notes

Connectez-vous pour exécuter votre code.

Tous les cours sont gratuits à lire. Créez un compte ou connectez-vous pour exécuter du code, lancer les tests et soumettre ce lab.

  • IDE dans le navigateur avec terminal bash — rien à installer
  • Correction instantanée, XP et votre place dans le parcours
Créer un compteSe connecter
OU
Continuer avec GitHub