σμ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.3·~45 minAvancé

Garanties de progression : de l'absence d'obstruction à l'absence d'attente

Position dans le parcoursModule 2 sur 4 · Sujet 3 sur 3
2.3 Garanties de progression : de l'absence d'obstruction à l'absence d'attente
—Score du lab— / 100XP total—Série—

5 min de lecture4 exemples de code

  1. 01Un thread en pause
  2. 02Sans verrou : quelqu'un finit toujours
  3. 03Sans attente : tout le monde finit
  4. 04Ce que vous allez construire

Trois compteurs donnent la bonne réponse sous tous les ordonnancements, et pourtant ils ne sont pas pareils. Mettez un thread en pause au pire moment : le verrou tournant arrête tout le monde, le compteur à comparer-et-échanger laisse passer quelqu'un, et le compteur fetch-and-add laisse chaque thread finir en 1 tour. Ce sont les trois échelons de l'échelle des garanties de progression.

01

Un thread en pause

Un vrai système d'exploitation peut retirer un thread de son cœur à tout moment : sa tranche de temps se termine, un défaut de page l'envoie sur le disque, une interruption arrive. Une bonne structure concurrente doit y survivre. Nous précisons la question avec un adversaire. Il exécute un prefix de tours de son choix, puis met un thread en pause pour toujours, et nous donnons un budget de tours à tous les autres. Si un thread non suspendu n'a toujours pas fini, la structure peut être arrêtée. Un verrou tournant échoue à ce test de la façon la plus simple : si l'adversaire met en pause le thread qui détient le verrou, les autres tournent jusqu'à la fin du budget.

main.pyPython
def locked_increments(k):    def thread(shared):        for _ in range(k):            while True:                yield                if not shared.get("lock", 0):                    shared["lock"] = 1                    break            yield            value = shared.get("x", 0)            yield            shared["x"] = value + 1            yield            shared["lock"] = 0    return threaddef progress_without(make_threads, prefix, paused, budget=50):    n = len(make_threads({}))    allowed = [tid for tid in range(n) if tid not in paused]    _, alive = run_prefix(make_threads, list(prefix) + allowed * budget)    return [tid for tid in allowed if tid not in alive]lk = lambda shared: [locked_increments(1)(shared), locked_increments(1)(shared)]print(run_prefix(lk, [0]))print("thread 0 paused holding the lock:", progress_without(lk, [0], [0]))print("thread 0 finished first:         ", progress_without(lk, [0, 0, 0, 0], [0]))
({'lock': 1}, [0, 1])
thread 0 paused holding the lock: []
thread 0 finished first:          [1]

Après un tour, le thread 0 détient le verrou ('lock': 1). Mettez-le en pause là et le thread 1 ne finit rien en cinquante tours : []. Laissez d'abord le thread 0 faire ses quatre tours et le verrou est de nouveau libre, donc le thread 1 finit : [1]. La différence tient seulement à l'endroit où l'adversaire frappe. Un verrou transforme toute pause dans la section critique en pause du programme entier.

02

Sans verrou : quelqu'un finit toujours

is_blocking automatise l'adversaire. Il essaie tous les préfixes de quelques tours, met en pause chaque thread vivant à tour de rôle, et demande à progress_without si les autres finissent encore. Pour le verrou tournant, la réponse est oui après un seul tour. Pour le compteur à comparer-et-échanger, jamais : un thread en pause entre sa lecture et son échange laisse simplement la valeur inchangée, donc l'échange de l'autre thread réussit. C'est la définition du sans verrou : le système dans son ensemble avance toujours, même si un thread est arrêté pour toujours.

main.pyPython
def is_blocking(make_threads, depth=3, budget=50):    def visit(prefix):        _, alive = run_prefix(make_threads, prefix)        for paused in alive:            finished = progress_without(make_threads, prefix, [paused], budget)            if len(finished) < len([tid for tid in range(len(make_threads({}))) if tid != paused]):                return True        if len(prefix) < depth:            return any(visit(prefix + [tid]) for tid in alive)        return False    return visit([])cs = lambda shared: [cas_increments(1)(shared), cas_increments(1)(shared)]at = lambda shared: [atomic_increments(1)(shared), atomic_increments(1)(shared)]print("spin lock:", is_blocking(lk))print("compare-and-swap:", is_blocking(cs))print("fetch-and-add:", is_blocking(at))
spin lock: True
compare-and-swap: False
fetch-and-add: False

Le compteur à comparer-et-échanger et le compteur fetch-and-add sont tous deux non bloquants. L'échelle a un troisième échelon parce que « sans verrou » permet encore une mauvaise journée à un thread : il peut perdre toutes les courses.

03

Sans attente : tout le monde finit

Une opération sans attente a une borne sur ses propres tours qu'aucun voisin ne peut dépasser. worst_own_turns la mesure : le plus grand nombre de tours qu'un thread prend sur tous les ordonnancements complets. classify pose alors la question décisive. Le pire cas du thread 0 reste-t-il le même quand son voisin a 1 incrément et quand il en a 3 ? Si oui, le thread 0 est sans attente. S'il croît, le voisin peut faire réessayer davantage le thread 0, donc la structure est seulement sans verrou.

main.pyPython
def worst_own_turns(make_threads, tid):    worst = 0    def visit(prefix):        nonlocal worst        _, alive = run_prefix(make_threads, prefix)        if not alive:            worst = max(worst, prefix.count(tid))            return        for other in alive:            visit(prefix + [other])    visit([])    return worstdef classify(factory, others=(1, 3)):    def program(m):        return lambda shared: [factory(1)(shared), factory(m)(shared)]    if is_blocking(program(others[0])):        return "blocking"    light, busy = worst_own_turns(program(others[0]), 0), worst_own_turns(program(others[1]), 0)    return "wait-free" if light == busy else "lock-free"for neighbour in (1, 2, 3):    cas = lambda shared: [cas_increments(1)(shared), cas_increments(neighbour)(shared)]    atomic = lambda shared: [atomic_increments(1)(shared), atomic_increments(neighbour)(shared)]    print(neighbour, "increments next door: CAS", worst_own_turns(cas, 0), "turns, fetch-and-add", worst_own_turns(atomic, 0))print([classify(f) for f in (locked_increments, cas_increments, atomic_increments)])
1 increments next door: CAS 4 turns, fetch-and-add 1
2 increments next door: CAS 6 turns, fetch-and-add 1
3 increments next door: CAS 8 turns, fetch-and-add 1
['blocking', 'lock-free', 'wait-free']
Lequel choisir

Sans attente est le plus fort et le plus coûteux à concevoir, et fetch-and-add montre qu'il n'est pas toujours hors de portée. Sans verrou est la cible pratique pour les files et les piles. Un verrou convient là où un détenteur en pause ne peut pas vous nuire, mais jamais sur un chemin qui ne doit pas se bloquer.

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 (("locked", locked_increments), ("cas", cas_increments), ("atomic", atomic_increments)):    program = lambda shared, f=factory: [f(k)(shared) for k in ups]    blocking = is_blocking(program)    print(name, classify(factory), blocking, None if blocking else worst_own_turns(program, 0))
locked blocking True None
cas lock-free False 8
atomic wait-free False 1
Ce que disent les vraies donnéesles jours de hausse des 6 premiers rendements SPY : 1 incrément pour le thread 0, 3 pour le thread 1
Verrou tournantblockingun thread en pause arrête l'autre
Comparer-et-échangerlock-freepire cas du thread 0 : 8 tours
Fetch-and-addwait-freepire cas du thread 0 : 1 tour
Pire nombre de tours propres du thread 0 selon le travail voisin
Fetch-and-add, 3 incréments voisins1
CAS, 1 incrément voisin4
CAS, 2 incréments voisins6
CAS, 3 incréments voisins8

Lisez les barres : le thread fetch-and-add a besoin de 1 tour quelle que soit l'activité de son voisin, alors que le thread à comparer-et-échanger en demande 4, 6, puis 8 à mesure que le voisin fait plus d'incréments. Chaque incrément supplémentaire à côté peut faire perdre une course de plus au thread 0 et lui faire répéter sa lecture et son échange. Cette croissance est exactement ce qui sépare sans verrou de sans attente.

DéfinitionGaranties de progression

Bloquant : un thread en pause peut arrêter les autres. Sans verrou : un thread finit toujours. Sans attente : chaque thread finit en un nombre borné de ses propres tours. Chaque niveau implique celui qui le précède dans cette liste.

04

Ce que vous allez construire

Le labo suit le cours : le compteur à verrou tournant, l'exécution avec des threads en pause, le test qui dit si la pause d'un thread peut arrêter les autres, le pire cas des tours propres d'un thread, la classification d'un compteur et l'étude des trois compteurs sur les jours de hausse de SPY.

  • Fonction · questionS'appuie sur
  • locked_increments(k)Un compteur qui attend un verrou ?De l'arithmétique simple
  • progress_without(make_threads, prefix, paused, budget)Qui finit si des threads sont en pause ?run_prefix
  • is_blocking(make_threads, depth, budget)Un thread en pause peut-il arrêter tout le monde ?progress_without
  • worst_own_turns(make_threads, tid)De combien de tours un thread peut-il avoir besoin ?run_prefix
  • classify(factory, others)Bloquant, sans verrou ou sans attente ?is_blockingworst_own_turns
  • study(bars)Où se situent les trois compteurs sur SPY ?classifyis_blockingworst_own_turns

À retenir

  • Un verrou peut arrêter tout le monde. Mettez le détenteur en pause après un tour et l'autre thread ne finit rien en cinquante tours.
  • Sans verrou veut dire que quelqu'un finit toujours. Mais un thread peut perdre toutes les courses : le comparer-et-échanger demande 4, 6, 8 tours quand le voisin travaille davantage.
  • Sans attente a une borne qu'aucun voisin ne casse. Fetch-and-add finit en 1 tour quoi que fasse le voisin.
Vérification rapide
Un thread fait un incrément par comparer-et-échanger pendant qu'un autre fait 5 incréments. Quelle affirmation est garantie ?
Choisissez une réponse.

Sujet suivant : vous savez maintenant ce qu'une file pourrait promettre. Un tampon circulaire à un producteur et un consommateur construit la file sans verrou utile la plus simple, avec un écrivain et un lecteur, une mémoire fixe et aucune lecture-modification-écriture atomique, et l'explorateur vérifie qu'elle ne perd ni ne réordonne jamais un tick.

Lab : ce qui est noté

6 tâches · 100 pts
  • Un compteur sur verrou tournant10 pts
  • Exécuter avec des threads en pause20 pts
  • Une pause peut-elle arrêter tout le monde20 pts
  • Le pire cas d'un thread15 pts
  • Nommer la garantie10 pts
  • L'étude sur la table SPY25 pts
Explorateur · lab-lf-05
▾ lab-lf-05
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-lf-05$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

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