σμ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 AcademyFeuilles de routeParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Feuilles de routeParcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμ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 AcademyFeuilles de routeParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Feuilles de routeParcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Lock-free›Module 1 · Courses critiques et explorateur›Sujet 1.1·~45 minIntermédiaire

Courses critiques et raison d'être des verrous

Position dans le parcoursModule 1 sur 4 · Sujet 1 sur 2
1.1 Courses critiques et raison d'être des verrous
—Score du lab— / 100XP total—Série—

6 min de lecture4 exemples de code

  1. 01Une ligne, trois étapes
  2. 02Un ordonnancement est une liste de numéros de threads
  3. 03Un verrou fait des étapes un seul bloc
  4. 04Ce que vous allez construire

Deux threads ajoutent 1 au même compteur, et le compteur finit faux, sans erreur ni plantage. La ligne x += 1 n'est pas une étape mais trois, et l'ordre dans lequel deux threads entrelacent ces étapes décide du résultat. Sur 1 085 jours de hausse de SPY, 542 mises à jour disparaissent ; un verrou les ramène toutes.

01

Une ligne, trois étapes

Un processeur ne peut pas ajouter à une valeur qui vit en mémoire. Il charge la valeur dans un registre, ajoute au registre, puis range le registre en mémoire. Donc x += 1 est load x, add 1, store x. Un thread seul fait les trois d'affilée et rien ne peut mal tourner. Avec deux threads, la machine peut exécuter une étape de l'un, puis une étape de l'autre, dans n'importe quel ordre. Pour le voir, nous écrivons une minuscule machine. Un thread est une liste d'étapes, avec son propre registre ; les threads partagent un dictionnaire de valeurs nommées. La fonction execute exécute une étape d'un thread. Ci-dessous nous jouons à la main les six étapes de deux threads, une étape chacun à tour de rôle.

main.pyPython
def execute(op, shared, regs, locks, tid):    kind = op[0]    if kind == "load":        regs[tid] = shared.get(op[1], 0)    elif kind == "add":        regs[tid] += op[1]    elif kind == "store":        shared[op[1]] = regs[tid]    elif kind == "lock":        if op[1] in locks:            return False        locks[op[1]] = tid    elif kind == "unlock":        del locks[op[1]]    return Trueshared, regs, locks = {"x": 0}, {0: 0, 1: 0}, {}turns = [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2), (1, 2)]1for tid, step in turns:    execute(INCREMENT[step], shared, regs, locks, tid)    print("thread", tid, INCREMENT[step], "->", shared, regs)
thread 0 ('load', 'x') -> {'x': 0} {0: 0, 1: 0}
thread 1 ('load', 'x') -> {'x': 0} {0: 0, 1: 0}
thread 0 ('add', 1) -> {'x': 0} {0: 1, 1: 0}
thread 1 ('add', 1) -> {'x': 0} {0: 1, 1: 1}
thread 0 ('store', 'x') -> {'x': 1} {0: 1, 1: 1}
thread 1 ('store', 'x') -> {'x': 1} {0: 1, 1: 1}
  1. 1Les deux threads chargent avant que l'un range : chaque registre contient 0.

Lisez la sortie ligne par ligne. Le thread 0 charge 0, le thread 1 charge 0 aussi (le thread 0 n'a pas encore rangé), les deux ajoutent 1 à un registre qui contient 0, et les deux rangent 1. Deux threads ont chacun fait leur x += 1, pourtant x vaut 1, pas 2. Une mise à jour a été perdue. Rien ne clochait dans le code de l'un ou l'autre thread ; le bug n'existe que dans cet ordre d'étapes.

DéfinitionCourse critique

Un bug dont le résultat dépend de l'ordre dans lequel les threads entrelacent leurs étapes. Le même programme est juste sous certains ordonnancements et faux sous d'autres, donc le bug est facile à rater en test.

02

Un ordonnancement est une liste de numéros de threads

Dans le monde réel, l'ordonnancement est choisi par le système d'exploitation et change d'une exécution à l'autre, ce qui rend une course si difficile à reproduire. Dans notre machine, nous le choisissons. Un ordonnancement est une liste de numéros de threads, et run donne un tour au thread nommé par chaque entrée. Deux ordonnancements simples comptent. Dans l'ordonnancement en série, le thread 0 exécute toutes ses étapes, puis le thread 1 : c'est ce qu'on espère. Dans l'ordonnancement en tourniquet, les threads font une étape chacun à tour de rôle : c'est le pire cas pour notre compteur, car les étapes des deux threads se chevauchent partout. La fonction lost_updates compare ce que les incréments auraient dû ajouter avec ce que montre le compteur.

main.pyPython
def run(programs, schedule, shared=None):    shared = dict(shared or {})    regs = {tid: 0 for tid in range(len(programs))}    pcs = [0] * len(programs)    locks = {}    for tid in schedule:        if pcs[tid] < len(programs[tid]) and execute(programs[tid][pcs[tid]], shared, regs, locks, tid):            pcs[tid] += 1    while any(pcs[t] < len(programs[t]) for t in range(len(programs))):        progress = False        for tid in range(len(programs)):            if pcs[tid] < len(programs[tid]) and execute(programs[tid][pcs[tid]], shared, regs, locks, tid):                pcs[tid] += 1                progress = True        if not progress:            raise RuntimeError("deadlock")    return shareddef serial_schedule(programs):    return [tid for tid, program in enumerate(programs) for _ in program]def round_robin_schedule(programs):    longest = max((len(program) for program in programs), default=0)    return [tid for step in range(longest) for tid, program in enumerate(programs) if step < len(program)]def lost_updates(programs, schedule, name="x"):    expected = sum(op[1] for program in programs for op in program if op[0] == "add")    return expected - run(programs, schedule).get(name, 0)two = [INCREMENT, INCREMENT]print(serial_schedule(two), run(two, serial_schedule(two)))print(round_robin_schedule(two), run(two, round_robin_schedule(two)))print("lost:", lost_updates(two, round_robin_schedule(two)))
[0, 0, 0, 1, 1, 1] {'x': 2}
[0, 1, 0, 1, 0, 1] {'x': 1}
lost: 1

L'ordonnancement en série [0, 0, 0, 1, 1, 1] donne 2 ; l'ordonnancement en tourniquet [0, 1, 0, 1, 0, 1] donne 1 et perd une mise à jour. Remarquez ce que fait run quand l'ordonnancement se termine trop tôt : les threads non terminés continuent à prendre des tours, une étape chacun dans l'ordre des threads, si bien que chaque programme finit toujours. Remarquez aussi qu'un thread terminé ignore ses tours, donc toute liste de numéros de threads est un ordonnancement valide.

03

Un verrou fait des étapes un seul bloc

Un verrou règle la course en interdisant le chevauchement. Un thread prend le verrou avant load et le libère après store. Un second thread qui demande le verrou entre-temps ne l'obtient pas : son tour ne fait rien et il reste à la même étape jusqu'à ce que le verrou soit libre. Les étapes entre lock et unlock s'exécutent donc comme un seul bloc, et aucun autre thread ne peut s'y glisser. La fonction counter_program construit le programme d'un thread qui incrémente un compteur, avec ou sans verrou. Les verrous apportent un nouveau danger, montré à la fin du code : deux threads qui prennent deux verrous dans des ordres opposés peuvent chacun détenir le verrou dont l'autre a besoin, et aucun ne peut plus avancer. C'est un interblocage, et notre run le détecte quand un passage complet ne fait aucun progrès.

main.pyPython
def counter_program(increments, locked=False):    program = []    for _ in range(increments):        program += ([("lock", "m")] if locked else []) + INCREMENT + ([("unlock", "m")] if locked else [])    return programsafe = [counter_program(1, locked=True), counter_program(1, locked=True)]print(counter_program(1, locked=True))print(run(safe, round_robin_schedule(safe)))ab = [[("lock", "a"), ("lock", "b"), ("unlock", "b"), ("unlock", "a")],      [("lock", "b"), ("lock", "a"), ("unlock", "a"), ("unlock", "b")]]try:    run(ab, [0, 1])except RuntimeError as error:    print("error:", error)
[('lock', 'm'), ('load', 'x'), ('add', 1), ('store', 'x'), ('unlock', 'm')]
{'x': 2}
error: deadlock
Un verrou n'est pas gratuit

Un thread qui attend un verrou ne fait rien, et le thread qui le détient peut être mis en pause par le système d'exploitation à n'importe quelle étape. Sur un chemin critique de trading, cela signifie une gigue que l'on ne peut pas borner. Le reste de cette compétence construit des structures qui n'ont besoin d'aucun verrou.

main.pyPython
def study(bars):    returns = [float(r) for r in bars["close"].pct_change().dropna()][:2000]    ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]    free = [counter_program(k) for k in ups]    locked = [counter_program(k, locked=True) for k in ups]    return {"up": sum(ups),            "serial": run(free, serial_schedule(free))["x"],            "racy": run(free, round_robin_schedule(free))["x"],            "locked": run(locked, round_robin_schedule(locked))["x"]}print(study(load_bars()))
{'up': 1085, 'serial': 1085, 'racy': 543, 'locked': 1085}
Ce que disent les vraies données2 000 rendements quotidiens SPY, deux threads comptant les jours de hausse, ordonnancement en tourniquet
Jours de hausse à compter1,085sur 2 000 jours
Ordonnancement en série1,085x à la fin : tout est compté
Tourniquet, sans verrou543x à la fin : 542 mises à jour perdues
Tourniquet, avec verrou1,085x à la fin : rien de perdu
Valeur finale du compteur selon l'ordonnancement
Série1085
Tourniquet, sans verrou543
Tourniquet, avec verrou1085

Sans verrou, l'ordonnancement en tourniquet garde les deux threads à une étape d'écart, si bien qu'ils chargent toujours la même valeur et que chaque paire d'incréments ne compte qu'une fois : environ la moitié des 1 085 jours de hausse est perdue. Un verrou rétablit le total exact, au prix de faire attendre un thread.

04

Ce que vous allez construire

Le labo suit le cours : une étape d'un thread, la machine entière sous un ordonnancement, les deux ordonnancements simples, le décompte des mises à jour perdues, le programme d'un thread compteur avec et sans verrou, et l'étude de deux threads qui comptent les jours de hausse de SPY.

  • Fonction · questionS'appuie sur
  • execute(op, shared, regs, locks, tid)Que fait une étape, et le thread doit-il attendre ?De l'arithmétique simple
  • run(programs, schedule, shared)Que laissent les threads sous cet ordonnancement ?execute
  • serial_schedule(programs)Un thread après l'autre ?De l'arithmétique simple
  • round_robin_schedule(programs)Une étape chacun à tour de rôle ?De l'arithmétique simple
  • lost_updates(programs, schedule)Combien d'incréments disparaissent ?run
  • counter_program(increments, locked)Que fait un thread compteur ?De l'arithmétique simple
  • study(bars)Combien de mises à jour une course coûte-t-elle sur SPY ?counter_programrunlost_updates

À retenir

  • `x += 1` fait trois étapes. Charger, ajouter, ranger : si deux threads chargent avant que l'un range, une mise à jour est perdue, sans aucune erreur pour vous prévenir.
  • Un bug est un ordonnancement. Une liste de numéros de threads peut être affichée, rejouée et testée ; 542 mises à jour sur 1 085 ont disparu sous un ordonnancement en tourniquet.
  • Un verrou guérit la course et ajoute un coût. Il fait des étapes un seul bloc, mais un thread qui attend ne fait rien, et deux verrous pris dans des ordres opposés provoquent un interblocage.
Vérification rapide
Deux threads exécutent chacun load x, add 1, store x avec x = 0, sous l'ordonnancement [0, 1, 1, 0, 0, 1]. Que vaut x à la fin ?
Choisissez une réponse.

Sujet suivant : vous avez trouvé la mise à jour perdue en choisissant un ordonnancement à la main. Un vrai programme a beaucoup trop d'ordonnancements pour les essayer à la main. Dans Un explorateur d'entrelacements, vous énumérez tous les ordonnancements d'un petit programme, comptez ceux qui le cassent, et gardez l'explorateur comme outil pour le reste de la compétence.

Lab : ce qui est noté

7 tâches · 100 pts
  • Une étape d'un thread15 pts
  • La machine sous un ordonnancement25 pts
  • Un thread après l'autre5 pts
  • Une étape chacun à tour de rôle10 pts
  • Mises à jour perdues10 pts
  • Un thread compteur10 pts
  • L'étude sur la table SPY25 pts
Explorateur · lab-lf-01
▾ lab-lf-01
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-lf-01$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-lf-01 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