σμ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.2·~1 hAvancé

Un explorateur d'entrelacements : trouver les bugs en énumérant les ordonnancements

Position dans le parcoursModule 1 sur 4 · Sujet 2 sur 2
1.2 Un explorateur d'entrelacements : trouver les bugs en énumérant les ordonnancements
—Score du lab— / 100XP total—Série—

5 min de lecture4 exemples de code

  1. 01Combien d'ordonnancements existe-t-il ?
  2. 02Les threads comme générateurs
  3. 03Explorer, puis poser une question
  4. 04Ce que vous allez construire

Essayer des ordonnancements à la main ne trouve un bug que si l'on devine le bon. Un explorateur exécute le programme sous tous les ordonnancements, liste tous les résultats et vous remet l'ordonnancement qui l'a cassé. Pour deux threads qui comptent les jours de hausse de dix rendements SPY, il y a 210 ordonnancements, et seuls 10 donnent le bon compte.

01

Combien d'ordonnancements existe-t-il ?

Un ordonnancement est un ordre de tours, et le thread t en prend exactement lengths[t]. Pour compter les ordres, on met tous les tours à la file (la somme de leurs longueurs), ce qui se range d'autant de façons que la factorielle de ce nombre ; puis on divise par la factorielle des tours de chaque thread, car échanger deux tours d'un même thread ne donne pas un nouvel ordonnancement. C'est le coefficient multinomial. Il croît vite : deux threads de 2 tours n'ont que 6 ordonnancements, trois threads de 3 tours en ont 1 680 et deux threads de 10 tours en ont 184 756. C'est pourquoi personne ne peut vérifier un programme concurrent en essayant des ordonnancements au hasard et en espérant.

main.pyPython
def count_schedules(lengths):    total = factorial(sum(lengths))    for n in lengths:        total //= factorial(n)    return totalprint(count_schedules([2, 2]))print(count_schedules([3, 3, 3]))print(count_schedules([10, 10]))
6
1680
184756
02

Les threads comme générateurs

Dans le labo précédent, un thread était une liste d'étapes. Le vrai code s'écrit mieux en Python ordinaire : désormais un thread est un générateur qui prend le dictionnaire partagé et exécute yield avant chaque accès aux données partagées. Un appel de next() fait alors avancer le thread jusqu'à son prochain accès et pas plus, si bien qu'un tour est exactement une étape avec laquelle un autre thread peut s'entrelacer. run_threads amorce chaque générateur (l'exécute jusqu'à son premier yield), donne un tour au thread nommé par chaque entrée de l'ordonnancement, et quand l'ordonnancement se termine laisse les threads non terminés finir dans l'ordre des threads. all_schedules liste tous les ordres de tours en choisissant quel thread passe en premier puis en récurrant sur le reste. Ci-dessous, les six ordonnancements de deux threads qui incrémentent une fois chacun.

main.pyPython
def all_schedules(lengths):    if sum(lengths) == 0:        return [[]]    out = []    for tid, n in enumerate(lengths):        if n:            rest = list(lengths)            rest[tid] -= 1            out += [[tid] + tail for tail in all_schedules(rest)]    return outdef run_threads(make_threads, schedule, shared=None):    shared = dict(shared or {})    threads = make_threads(shared)    alive = [True] * len(threads)    def turn(tid):        try:            next(threads[tid])        except StopIteration:            alive[tid] = False    for tid in range(len(threads)):        turn(tid)    for tid in schedule:        if alive[tid]:            turn(tid)    while any(alive):        for tid in range(len(threads)):            if alive[tid]:                turn(tid)    return sharedtwo = lambda shared: [increments(1)(shared), increments(1)(shared)]for schedule in all_schedules([2, 2]):    print(schedule, run_threads(two, schedule))
[0, 0, 1, 1] {'x': 2}
[0, 1, 0, 1] {'x': 1}
[0, 1, 1, 0] {'x': 1}
[1, 0, 0, 1] {'x': 1}
[1, 0, 1, 0] {'x': 1}
[1, 1, 0, 0] {'x': 2}

Deux des six ordonnancements finissent à 2, les quatre autres à 1. Les deux bons sont ceux où un thread fait ses deux étapes avant que l'autre commence. Dans les quatre autres, les deux threads lisent avant que l'un écrive, ce qui est exactement la mise à jour perdue du labo précédent, trouvée cette fois sans deviner.

03

Explorer, puis poser une question

explore exécute le programme sous tous les ordonnancements et compte combien donnent chaque état final. La clé est un tuple trié de paires (nom, valeur) pour que des états égaux soient la même clé. find_schedule pose la question d'un rapport de bug : quel ordonnancement casse mon invariant ? Il renvoie le premier, et comme run_threads est déterministe on peut le rejouer, l'afficher ou le coller dans un test. S'il renvoie None, l'invariant tient sous tous les ordonnancements, ce qui est une preuve pour ce petit programme, pas un espoir.

main.pyPython
def explore(make_threads, lengths, shared=None):    outcomes = {}    for schedule in all_schedules(lengths):        key = tuple(sorted(run_threads(make_threads, schedule, shared).items()))        outcomes[key] = outcomes.get(key, 0) + 1    return outcomesdef find_schedule(make_threads, lengths, check, shared=None):    for schedule in all_schedules(lengths):        if not check(run_threads(make_threads, schedule, shared)):            return schedule    return Noneprint(explore(two, [2, 2]))print(find_schedule(two, [2, 2], lambda shared: shared["x"] == 2))
{(('x', 2),): 2, (('x', 1),): 4}
[0, 1, 0, 1]
L'espace croît de façon factorielle

L'énumération par force brute marche pour quelques tours par thread puis explose. Les vrais explorateurs élaguent : ils retiennent les états déjà vus et sautent les ordonnancements qui ne font que réordonner des étapes sans effet mutuel. L'idée reste la même, donc ce que vous construisez ici est le bon modèle mental.

main.pyPython
returns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:10]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]lengths = [2 * k for k in ups]many = lambda shared: [increments(k)(shared) for k in ups]print(ups, count_schedules(lengths))print({dict(key)["x"]: n for key, n in explore(many, lengths).items()})
[2, 3] 210
{5: 10, 4: 70, 3: 104, 2: 26}
Ce que disent les vraies donnéesles 10 premiers rendements SPY, deux threads comptant les jours de hausse
Ordonnancements210pour 2 et 3 jours de hausse
Bonne réponse10ordonnancements finissent à x = 5
Perdent des mises à jour200sur 210 ordonnancements
Pire cas2x à la fin, sur 5
Ordonnancements selon la valeur finale de x
26
104
70
10

La bonne réponse, x = 5, n'apparaît que dans 10 ordonnancements sur 210, soit environ 5 %. Un test qui exécute le programme une fois, ou cent fois sur une vraie machine où les threads se chevauchent rarement de cette façon, passerait presque à chaque fois et livrerait quand même un bug. L'explorateur ne peut pas le rater, et find_schedule vous remet le premier coupable : [0, 0, 0, 1, 0, 1, 1, 1, 1, 1].

DéfinitionInvariant

Une affirmation sur les données qui doit tenir quel que soit l'ordonnancement. Un programme concurrent est correct quand son invariant tient sous tous les ordonnancements, et explorer tous les ordonnancements est la façon de le vérifier sur un petit cas.

04

Ce que vous allez construire

Le labo suit le cours : le nombre d'ordonnancements, la liste de tous, la machine qui exécute des threads générateurs sous un ordonnancement, l'explorateur qui compte les résultats, la recherche du premier ordonnancement qui casse un invariant et l'étude de deux threads qui comptent les jours de hausse de SPY.

  • Fonction · questionS'appuie sur
  • count_schedules(lengths)Combien d'ordonnancements le programme a-t-il ?De l'arithmétique simple
  • all_schedules(lengths)Lesquels, dans l'ordre ?De l'arithmétique simple
  • run_threads(make_threads, schedule, shared)Que laisse un ordonnancement ?De l'arithmétique simple
  • explore(make_threads, lengths, shared)Quels résultats existent, et combien de fois ?all_schedulesrun_threads
  • find_schedule(make_threads, lengths, check)Quel ordonnancement casse l'invariant en premier ?all_schedulesrun_threads
  • study(bars)Combien d'ordonnancements du comptage SPY tournent mal ?count_schedulesexplorefind_schedule

À retenir

  • Comptez d'abord les ordonnancements. Le coefficient multinomial croît de façon factorielle : 6 ordonnancements pour deux threads de 2 tours, 184 756 pour deux threads de 10.
  • Un yield marque chaque accès partagé. Écrire les threads en générateurs fait d'un tour un accès, si bien que l'explorateur peut essayer tous les entrelacements de code Python ordinaire.
  • Un invariant plus tous les ordonnancements font une preuve. Seuls 10 ordonnancements sur 210 ont donné le bon compte ; find_schedule renvoie le premier qui ne le donne pas, prêt à rejouer.
Vérification rapide
Deux threads prennent 3 tours chacun. Combien d'ordonnancements différents ont-ils ?
Choisissez une réponse.

Sujet suivant : l'explorateur montre que le bug est réel ; il faut maintenant le réparer. Atomiques et lecture-modification-écriture ajoute les opérations qu'un processeur peut faire en une seule étape indivisible, comme fetch-and-add et compare-and-swap, et vous vérifiez avec l'explorateur que le compteur est juste sous tous les ordonnancements.

Lab : ce qui est noté

6 tâches · 100 pts
  • Combien d'ordonnancements10 pts
  • Chaque ordonnancement15 pts
  • Exécuter les threads25 pts
  • Compter chaque résultat20 pts
  • Le premier mauvais ordonnancement10 pts
  • L'étude sur la table SPY20 pts
Explorateur · lab-lf-02
▾ lab-lf-02
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-lf-02$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

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