σμ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 2 · Atomiques, ordre et garanties›Sujet 2.1·~55 minAvancé

Atomiques et lecture-modification-écriture

Position dans le parcoursModule 2 sur 4 · Sujet 1 sur 3
2.1 Atomiques et lecture-modification-écriture
—Score du lab— / 100XP total—Série—

5 min de lecture4 exemples de code

  1. 01Un tour, aucun intervalle
  2. 02Comparer-et-échanger et la boucle de réessai
  3. 03Explorer quand le nombre de tours varie
  4. 04Ce que vous allez construire

Une mise à jour perdue arrive parce qu'une lecture et une écriture sont deux tours séparés par un intervalle. Une opération atomique ferme cet intervalle : la lecture, la modification et l'écriture se font en une seule étape indivisible. Avec elle, le compteur est juste sous tous les ordonnancements : 4 ordonnancements au lieu de 28, et aucun n'est faux.

01

Un tour, aucun intervalle

Dans notre machine, un tour va d'un yield au suivant, donc rien de ce qui est écrit entre deux yield ne peut être entrelacé. Une fonction qui ne contient aucun yield se comporte donc comme une opération atomique. La plus courante est fetch-and-add : elle lit l'ancienne valeur, écrit old + n et renvoie old, le tout dans un seul tour. Un thread compteur qui l'appelle n'a qu'un yield pour tout l'incrément au lieu de deux, si bien que les deux threads ne peuvent plus lire la même ancienne valeur.

main.pyPython
def fetch_add(shared, name, n):    old = shared.get(name, 0)    shared[name] = old + n    return olddef atomic_increments(k):    def thread(shared):        for _ in range(k):            yield            fetch_add(shared, "x", 1)    return threadtwo = lambda shared: [atomic_increments(1)(shared), atomic_increments(1)(shared)]for prefix in ([0, 1], [1, 0]):    print(prefix, run_prefix(two, prefix))
[0, 1] ({'x': 2}, [])
[1, 0] ({'x': 2}, [])

Les deux ordres finissent à {'x': 2} et aucun thread n'est resté vivant. Avec un tour par incrément, le programme n'a que les deux ordonnancements [0, 1] et [1, 0] (4 dans l'étude, pour 1 et 3 incréments), et aucun ne peut perdre une mise à jour.

02

Comparer-et-échanger et la boucle de réessai

Fetch-and-add ne sait qu'ajouter. Pour tout le reste il faut l'outil général, comparer-et-échanger (CAS). Le motif est une boucle : lire l'ancienne valeur, en calculer la nouvelle, et demander au CAS de l'installer seulement si personne n'a changé la valeur entre-temps. Si le CAS échoue, quelqu'un est passé avant, donc on relit et on réessaie. Ci-dessous, deux compteurs CAS : quand le thread 1 lit avant que le thread 0 ait écrit, son CAS échoue une fois, il compte un réessai, et la seconde tentative réussit. La mise à jour n'est jamais perdue, car une écriture périmée est refusée.

main.pyPython
def compare_and_swap(shared, name, expected, new):    if shared.get(name, 0) != expected:        return False    shared[name] = new    return Truedef cas_increments(k):    def thread(shared):        for _ in range(k):            while True:                yield                old = shared.get("x", 0)                yield                if compare_and_swap(shared, "x", old, old + 1):                    break                fetch_add(shared, "retries", 1)    return threadcas = lambda shared: [cas_increments(1)(shared), cas_increments(1)(shared)]print(run_prefix(cas, [0, 1, 0, 1]))print(run_prefix(cas, [0, 1, 0, 1, 1, 1]))
({'x': 1, 'retries': 1}, [1])
({'x': 2, 'retries': 1}, [])

Après [0, 1, 0, 1], le dictionnaire partagé est {'x': 1, 'retries': 1} et le thread 1 est encore vivant : il avait lu 0, mais le thread 0 avait déjà rangé 1, donc son comparer-et-échanger a été refusé. En donnant au thread 1 ses deux tours suivants (relire, réessayer), on finit à x = 2. Notez le prix : le perdant a fait le travail deux fois. Fetch-and-add ne réessaie jamais ; le CAS réessaie autant de fois que les threads se heurtent.

03

Explorer quand le nombre de tours varie

L'explorateur du sujet précédent supposait que chaque thread prend un nombre fixe de tours, lengths. Une boucle de réessai casse cela : un thread qui perd une course prend des tours supplémentaires, et leur nombre dépend de l'ordonnancement. La solution est de parcourir les ordonnancements comme un arbre. À partir d'un préfixe de tours, run_prefix rejoue le programme et dit quels threads sont encore vivants ; s'il n'y en a aucun, l'ordonnancement est complet et son état final est compté, sinon on se ramifie sur chacun d'eux. Cette recherche en profondeur visite chaque ordonnancement complet exactement une fois. values_of lit ensuite un nom partagé dans tous les résultats.

main.pyPython
def explore_all(make_threads, shared=None):    outcomes = {}    def visit(prefix):        final, alive = run_prefix(make_threads, prefix, shared)        if not alive:            key = tuple(sorted(final.items()))            outcomes[key] = outcomes.get(key, 0) + 1            return        for tid in alive:            visit(prefix + [tid])    visit([])    return outcomesdef values_of(outcomes, name):    return sorted({dict(key).get(name, 0) for key in outcomes})print(explore_all(cas))racy = lambda shared: [increments(1)(shared), increments(1)(shared)]print(values_of(explore_all(racy), "x"), values_of(explore_all(cas), "x"))
{(('x', 2),): 2, (('retries', 1), ('x', 2)): 4}
[1, 2] [2]
Atomique ne veut pas dire gratuit

Dans cette machine, chaque opération est vue par tous les threads en même temps et dans l'ordre. Un vrai processeur ne le promet pas pour les lectures et écritures ordinaires, et même une opération atomique oblige les cœurs à se disputer la ligne de cache. Le sujet suivant lève la première hypothèse.

main.pyPython
returns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:6]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]for name, factory in (("racy", increments), ("atomic", atomic_increments), ("cas", cas_increments)):    outcomes = explore_all(lambda shared, f=factory: [f(k)(shared) for k in ups])    print(name, sum(outcomes.values()), "schedules, x in", values_of(outcomes, "x"))
racy 28 schedules, x in [1, 2, 3, 4]
atomic 4 schedules, x in [4]
cas 144 schedules, x in [4]
Ce que disent les vraies donnéesles 6 premiers rendements SPY, un thread avec 1 jour de hausse et un avec 3
Compteur avec course24des 28 ordonnancements sont faux
fetch-and-add0des 4 ordonnancements sont faux
Comparer-et-échanger0des 144 ordonnancements sont faux
Pire nombre de réessais3réessais du compteur CAS
Ordonnancements par compteur
fetch-and-add4
Avec course28
Comparer-et-échanger144

Les deux réparations sont justes sous tous les ordonnancements, et elles diffèrent par le coût. Fetch-and-add fait le travail en un tour par incrément : 4 ordonnancements. Le compteur CAS a 144 ordonnancements, car une collision ajoute des tours, et dans le pire un thread réessaie 3 fois. C'est le compromis du code sans verrou : personne n'attend un verrou, mais le gagnant de chaque course oblige le perdant à refaire son travail.

DéfinitionLecture-modification-écriture

Une opération qui lit une valeur partagée, en calcule une nouvelle et la réécrit en une seule étape indivisible. Fetch-and-add et comparer-et-échanger sont les deux que vous utiliserez le plus ; toute structure sans verrou est bâtie avec elles.

04

Ce que vous allez construire

Le labo suit le cours : fetch-and-add, comparer-et-échanger, un thread compteur bâti sur chacun, l'explorateur qui gère des threads à nombre de tours variable, un utilitaire qui lit les valeurs d'un nom sur tous les résultats et l'étude de trois compteurs sur les jours de hausse de SPY.

  • Fonction · questionS'appuie sur
  • fetch_add(shared, name, n)Ajouter n, et qu'y avait-il avant ?De l'arithmétique simple
  • compare_and_swap(shared, name, expected, new)La valeur valait-elle encore ce que j'ai lu ?De l'arithmétique simple
  • atomic_increments(k)Un compteur sans intervalle ?fetch_add
  • cas_increments(k)Un compteur qui réessaie au lieu de perdre ?compare_and_swapfetch_add
  • explore_all(make_threads, shared)Tous les résultats, quel que soit le nombre de tours ?run_prefix
  • values_of(outcomes, name)Quelles valeurs ce nom prend-il ?De l'arithmétique simple
  • study(bars)Comment les trois compteurs se comparent-ils sur SPY ?explore_allvalues_of

À retenir

  • Atomique veut dire aucun yield à l'intérieur. Une lecture-modification-écriture en un tour ne peut pas être entrelacée, donc fetch-and-add laisse 4 ordonnancements, tous justes.
  • Le CAS transforme un conflit en réessai. Une écriture périmée est refusée au lieu d'être perdue : 144 ordonnancements, aucun faux, au plus 3 réessais.
  • Explorez en arbre quand les tours varient. Rejouez chaque préfixe et ramifiez-vous sur les threads encore vivants ; chaque ordonnancement complet est visité une fois.
Vérification rapide
Le thread 0 et le thread 1 exécutent chacun un incrément par comparer-et-échanger. Tous deux lisent x = 0 avant que l'un échange. Que se passe-t-il ensuite ?
Choisissez une réponse.

Sujet suivant : nous avons supposé que chaque thread voit chaque écriture immédiatement et dans l'ordre. Un vrai processeur ne le promet pas. Le modèle mémoire : acquire et release ajoute un tampon d'écriture à la machine, montre un lecteur qui voit un drapeau avant les données qu'il protège, et le répare avec l'ordre le moins coûteux.

Lab : ce qui est noté

7 tâches · 100 pts
  • Fetch-and-add5 pts
  • Comparer-et-échanger10 pts
  • Un compteur avec fetch-and-add10 pts
  • Un compteur avec boucle de réessai20 pts
  • Explorer tous les ordonnancements20 pts
  • Les valeurs d'un nom5 pts
  • L'étude sur la table SPY30 pts
Explorateur · lab-lf-03
▾ lab-lf-03
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-lf-03$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

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