σμ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 4 · Tests et sûreté mémoire›Sujet 4.2·~1 h 10 minExpert

Pointeurs de danger et récupération par époques

Position dans le parcoursModule 4 sur 4 · Sujet 2 sur 3
4.2 Pointeurs de danger et récupération par époques
—Score du lab— / 100XP total—Série—

5 min de lecture3 exemples de code

  1. 01Libérer tout de suite : le dépilage dangereux
  2. 02Pointeurs de danger : annoncer, puis vérifier
  3. 03Époques : annoncer quand on est entré
  4. 04Ce que vous allez construire

Une pile sans verrou ne bloque jamais, mais elle ne peut pas simplement libérer un nœud qu'elle a dépilé : un autre thread a pu lire la tête il y a un instant et s'apprêter à lire le next de ce nœud. La mémoire libérée peut être réutilisée ou retirée, donc cette lecture est un usage après libération. Il existe deux remèdes classiques. Les pointeurs de danger laissent un thread annoncer le nœud qu'il utilise. Les époques lui font annoncer quand il est entré. Dans les deux cas, un nœud détaché n'est libéré que lorsque plus personne ne peut le tenir.

01

Libérer tout de suite : le dépilage dangereux

Le modèle est la pile du sujet ABA. Un thread lit la tête, lit le nœud, puis échange la tête en un seul compare-and-swap. Le dépilage dangereux libère le nœud dans le tour même où l'échange réussit. Un nœud libéré est un nœud que personne ne doit toucher, et read_node enregistre un usage après libération chaque fois qu'un thread en lit un. Deux threads qui dépilent une fois chacun donnent seulement 20 ordonnancements, et l'explorateur trouve aussitôt les mauvais : ceux où un thread a lu la tête, l'autre a dépilé et libéré ce nœud, et le premier le lit ensuite.

main.pyPython
def pop_unsafe(shared, tid):    while True:        yield        idx = shared["head"]        if idx == -1:            return None        yield        nxt, value = read_node(shared, idx)        yield        if compare_and_swap(shared, "head", idx, nxt):            shared["popped"] = shared["popped"] + (value,)            free_node(shared, idx)            return valuedef two_pops(shared):    return [pop_unsafe(shared, 0), pop_unsafe(shared, 1)]outcomes = explore_all(two_pops, make_heap([9369, 9305]))hits = sum(n for key, n in outcomes.items() if dict(key)["uaf"])print(sum(outcomes.values()), "schedules,", hits, "of them read a freed node")
20 schedules, 6 of them read a freed node
02

Pointeurs de danger : annoncer, puis vérifier

Un pointeur de danger transforme la libération en dialogue. Avant de lire un nœud, le thread écrit son numéro dans son emplacement. Cela ne suffit pas, car le nœud a pu être dépilé et libéré entre la lecture de la tête et l'écriture de l'emplacement ; le thread valide donc : il relit la tête et recommence si elle a bougé. Après un dépilage réussi, le thread ne libère pas le nœud, il le met dans sa propre liste de retraite et balaie : il lit une fois l'emplacement de chaque thread et ne libère que les nœuds retirés qu'aucun emplacement ne nomme. Les nœuds restants attendent un balayage ultérieur.

main.pyPython
def pop_hazard(shared, tid):    while True:        yield        idx = shared["head"]        if idx == -1:            shared["hp%d" % tid] = -1            return None        yield        shared["hp%d" % tid] = idx        yield        if shared["head"] != idx:            continue        yield        nxt, value = read_node(shared, idx)        yield        if compare_and_swap(shared, "head", idx, nxt):            shared["popped"] = shared["popped"] + (value,)            shared["hp%d" % tid] = -1            shared["retired%d" % tid] = shared["retired%d" % tid] + (idx,)            break    yield    hazards = tuple(shared["hp%d" % t] for t in range(shared["threads"]))    yield    keep = []    for node in shared["retired%d" % tid]:        if node in hazards:            keep.append(node)        else:            free_node(shared, node)    shared["retired%d" % tid] = tuple(keep)    return valuedef two_pops(shared):    return [pop_hazard(shared, 0), pop_hazard(shared, 1)]outcomes = explore_all(two_pops, make_heap([9369, 9305]))hits = sum(n for key, n in outcomes.items() if dict(key)["uaf"])waiting = max(sum(len(dict(key)["retired%d" % t]) for t in (0, 1)) for key in outcomes)print(sum(outcomes.values()), "schedules,", hits, "read a freed node, up to", waiting, "node waiting at the end")
12122 schedules, 0 read a freed node, up to 1 node waiting at the end
DéfinitionPointeur de danger

Une annonce propre à chaque thread du seul nœud en cours de lecture, écrite avant la lecture et validée après. Un nœud retiré n'est libéré que si aucune annonce ne le nomme.

03

Époques : annoncer quand on est entré

La récupération par époques annonce bien moins. Il y a un compteur global d'époque. Un thread écrit l'époque courante dans son emplacement en entrant dans une opération et -1 en la quittant, et n'annonce jamais de nœud individuel. Un nœud retiré à l'époque e ne peut être tenu que par un thread entré à l'époque e ou avant ; on peut donc le libérer quand chaque thread dans une opération a annoncé une époque supérieure à e. Le prix : un thread resté longtemps à l'intérieur retient tous les nœuds retirés après son entrée, alors qu'un pointeur de danger ne retient que le nœud qu'il nomme.

main.pyPython
def pop_epoch(shared, tid):    yield    shared["ep%d" % tid] = shared["epoch"]    while True:        yield        idx = shared["head"]        if idx == -1:            shared["ep%d" % tid] = -1            return None        yield        nxt, value = read_node(shared, idx)        yield        if compare_and_swap(shared, "head", idx, nxt):            shared["popped"] = shared["popped"] + (value,)            shared["ep%d" % tid] = -1            shared["retired%d" % tid] = shared["retired%d" % tid] + ((idx, shared["epoch"]),)            shared["epoch"] = shared["epoch"] + 1            break    announced = []    for t in range(shared["threads"]):        yield        if shared["ep%d" % t] != -1:            announced.append(shared["ep%d" % t])    yield    keep = []    for node, epoch in shared["retired%d" % tid]:        if announced and epoch >= min(announced):            keep.append((node, epoch))        else:            free_node(shared, node)    shared["retired%d" % tid] = tuple(keep)    return valuedef two_pops(shared):    return [pop_epoch(shared, 0), pop_epoch(shared, 1)]outcomes = explore_all(two_pops, make_heap([9369, 9305]))hits = sum(n for key, n in outcomes.items() if dict(key)["uaf"])waiting = max(sum(len(dict(key)["retired%d" % t]) for t in (0, 1)) for key in outcomes)print(sum(outcomes.values()), "schedules,", hits, "read a freed node, up to", waiting, "node waiting at the end")
9012 schedules, 0 read a freed node, up to 1 node waiting at the end
Une fuite n'est pas un bug, un thread bloqué en est un

Les deux méthodes laissent des nœuds retirés en attente à la fin de certains ordonnancements : l'explorateur les signale comme pending. Un balayage ultérieur les libère, donc rien n'est perdu. Le vrai danger des époques est un thread qui reste bloqué dans une opération : il n'annonce jamais d'époque plus récente, aucun nœud retiré après lui n'est jamais libéré et la mémoire croît. Les pointeurs de danger y échappent, car un thread bloqué ne retient qu'un seul nœud.

Ce que disent les vraies donnéesdeux threads dépilant chacun une fois une pile des deux premières clôtures SPY (9 369 et 9 305 centimes)
Libérer tout de suite206 ordonnancements lisent un nœud libéré
Pointeurs de danger12,122aucun ne lit un nœud libéré
Époques9,012aucun ne lit un nœud libéré
Nœuds laissés en attente1au plus, pour les deux méthodes sûres

La sûreté coûte des ordonnancements, car chaque annonce, validation et balayage est une étape de plus où l'autre thread peut s'insérer : 20 est devenu 12 122 et 9 012. C'est exactement pourquoi l'explorateur est utile ici. Il n'a pas besoin de savoir pourquoi la méthode est correcte, il vérifie chacun de ces ordonnancements.

04

Ce que vous allez construire

Le labo suit le cours : l'état partagé avec un jeu d'emplacements par thread, le dépilage dangereux, le dépilage à pointeurs de danger, le dépilage à époques, la vérification des deux invariants (aucun usage après libération, rien de perdu), le vérificateur qui explore tous les ordonnancements d'une méthode, et l'étude des trois méthodes sur les clôtures SPY.

  • Fonction · questionS'appuie sur
  • make_heap(values, threads)Qu'est-ce qui est partagé ?De l'arithmétique simple
  • pop_unsafe(shared, tid)Dépiler et libérer tout de suiteDe l'arithmétique simple
  • pop_hazard(shared, tid)Annoncer, valider, retirer, balayerDe l'arithmétique simple
  • pop_epoch(shared, tid)Entrer, retirer avec l'époque, récupérerDe l'arithmétique simple
  • violations(final)Quels invariants sont brisés ?De l'arithmétique simple
  • verify(scheme, values, pops)Une méthode est-elle sûre sous tous les ordonnancements ?explore_allviolations
  • study(bars)Quelle méthode survit à la pile SPY ?verify

À retenir

  • Libérer est un protocole. Détacher un nœud ne le rend pas sûr à libérer : quelqu'un peut encore être sur le point de le lire.
  • Un pointeur de danger nomme un nœud ; une époque nomme un moment. Les pointeurs de danger exigent une validation après l'annonce ; les époques coûtent moins mais un thread bloqué retient tout.
  • Laissez l'explorateur trouver la faille. Retirez la validation et des ordonnancements sur 12 122 lisent de la mémoire libérée.
Vérification rapide
Le thread A a lu la tête (nœud 0) mais ne l'a pas encore annoncée. Le thread B dépile le nœud 0 et balaie. Que doit faire le thread A après avoir annoncé le nœud 0 ?
Choisissez une réponse.

Sujet suivant : c'était une pile, et un jouet. Le projet final une diffusion de données de marché sans verrou met toute la compétence au travail : des ticks SPY vont d'un producteur à plusieurs consommateurs par une structure sans verrou, et l'explorateur prouve ses invariants sous tous les ordonnancements.

Lab : ce qui est noté

7 tâches · 100 pts
  • L'état partagé5 pts
  • Libérer tout de suite10 pts
  • Pointeurs de danger20 pts
  • Époques20 pts
  • Les invariants10 pts
  • Le vérificateur15 pts
  • L'étude sur la table SPY20 pts
Explorateur · lab-lf-10
▾ lab-lf-10
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-lf-10$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

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