σμ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 3 · Files sans verrou›Sujet 3.3·~1 h 10 minAvancé

Files à plusieurs producteurs et plusieurs consommateurs

Position dans le parcoursModule 3 sur 4 · Sujet 3 sur 3
3.3 Files à plusieurs producteurs et plusieurs consommateurs
—Score du lab— / 100XP total—Série—

5 min de lecture4 exemples de code

  1. 01Un ticket n'est pas une livraison
  2. 02Vérifier tous les ordonnancements
  3. 03Ce que le numéro de séquence apporte
  4. 04Ce que vous allez construire

Avec un producteur et un consommateur, une position a un seul écrivain. Avec plusieurs, les positions doivent être distribuées atomiquement, et un ticket ne suffit pas : le producteur qui détient la position 5 n'a pas encore écrit son élément. Un numéro de séquence sur chaque case dit à qui est le tour de la case. Sur 2 756 ordonnancements de deux producteurs et 1 600 de deux consommateurs, cette file ne se casse jamais.

01

Un ticket n'est pas une livraison

Une file bornée de capacity cases utilise deux compteurs qui ne font que croître. Un producteur prend la position suivante par un comparer-et-échanger sur enq ; un consommateur prend la suivante par un comparer-et-échanger sur deq. La position p vit dans la case p % capacity. Le comparer-et-échanger rend les réservations sûres : deux producteurs n'obtiennent jamais la même position. Mais réserver une position n'est pas écrire l'élément. Entre l'échange et l'écriture, la case est réservée et vide, et un consommateur qui la lirait maintenant lirait n'importe quoi. Chaque case porte donc un numéro de séquence qui n'avance qu'au bon moment.

main.pyPython
def init_queue(capacity):    shared = {"enq": 0, "deq": 0}    for cell in range(capacity):        shared["seq%d" % cell] = cell    return shareddef enqueue(shared, item, capacity):    while True:        yield        pos = shared["enq"]        cell = pos % capacity        diff = shared["seq%d" % cell] - pos        if diff < 0:            return False        if diff > 0:            continue        yield        if compare_and_swap(shared, "enq", pos, pos + 1):            break    yield    shared["data%d" % cell] = item    shared["seq%d" % cell] = pos + 1    shared["put%d" % pos] = item    return Truedef dequeue(shared, capacity):    while True:        yield        pos = shared["deq"]        cell = pos % capacity        diff = shared["seq%d" % cell] - (pos + 1)        if diff < 0:            return None        if diff > 0:            continue        yield        if compare_and_swap(shared, "deq", pos, pos + 1):            break    yield    item = shared.get("data%d" % cell)    shared["seq%d" % cell] = pos + capacity    shared["got%d" % pos] = item    return itemsolo = lambda shared: [producer_thread((7,), 2)(shared), consumer_thread(1, 2)(shared)]for prefix in ([0, 0, 0], [0, 0, 0, 1, 1, 1], [0, 0, 1, 1, 0, 1, 1]):    print(prefix, run_prefix(solo, prefix, init_queue(2))[0])
[0, 0, 0] {'enq': 1, 'deq': 0, 'seq0': 1, 'seq1': 1, 'data0': 7, 'put0': 7}
[0, 0, 0, 1, 1, 1] {'enq': 1, 'deq': 1, 'seq0': 2, 'seq1': 1, 'data0': 7, 'put0': 7, 'got0': 7}
[0, 0, 1, 1, 0, 1, 1] {'enq': 1, 'deq': 0, 'seq0': 1, 'seq1': 1, 'data0': 7, 'put0': 7}

Lisez les trois exécutions. Après [0, 0, 0], le producteur a réservé la position 0 (enq: 1), rangé data0 et mis seq0 à 1 : l'élément est prêt. Après les trois tours du consommateur, got0: 7 et seq0: 2 : la case est libre pour le tour suivant. Dans la dernière exécution, le consommateur regarde entre la réservation du producteur et sa publication, voit seq0 = 0 au lieu de 1, et repart les mains vides. Il ne lit pas n'importe quoi. Il dit simplement « rien de prêt pour l'instant », et l'élément sera trouvé par une tentative ultérieure.

02

Vérifier tous les ordonnancements

Que doit promettre la file maintenant ? Les producteurs consignent chaque élément sous sa position (put3), les consommateurs consignent ce qu'ils ont pris sous la position réservée (got3). Si la file fonctionne, chaque position sortie contient l'élément qui y a été mis (pas de mismatch), et les positions sorties sont 0, 1, 2, ... sans trou ni répétition (pas de gap). Cela donne à la file son ordre : l'ordre des positions. violations lit ces deux faits dans un état final et verify lance l'explorateur pour un nombre quelconque de producteurs et de consommateurs. Ici, un seul producteur de deux éléments contre deux consommateurs, et un seul élément contre un consommateur.

main.pyPython
def violations(final):    puts = {int(key[3:]): value for key, value in final.items() if key.startswith("put")}    gots = {int(key[3:]): value for key, value in final.items() if key.startswith("got")}    broken = []    if any(gots[p] != puts.get(p) for p in gots):        broken.append("mismatch")    if sorted(gots) != list(range(len(gots))):        broken.append("gap")    return sorted(broken)def verify(producers, consumers, capacity):    def make_threads(shared):        return [producer_thread(items, capacity)(shared) for items in producers] + [consumer_thread(n, capacity)(shared) for n in consumers]    outcomes = explore_all(make_threads, init_queue(capacity))    broken = {}    pushed = popped = 0    for key, count in outcomes.items():        final = dict(key)        pushed = max(pushed, sum(1 for k in final if k.startswith("put")))        popped = max(popped, sum(1 for k in final if k.startswith("got")))        for name in violations(final):            broken[name] = broken.get(name, 0) + count    return {"schedules": sum(outcomes.values()), "broken": dict(sorted(broken.items())), "pushed": pushed, "popped": popped}print(verify([(7,)], [1], 2))print(verify([(7, 8)], [1, 1], 2))
{'schedules': 4, 'broken': {}, 'pushed': 1, 'popped': 1}
{'schedules': 1600, 'broken': {}, 'pushed': 2, 'popped': 2}
DéfinitionNuméro de séquence

Un compteur sur chaque case qui indique à qui est le tour. Il ne change que lorsque l'élément est écrit (à p + 1) et lorsqu'il est pris (à p + capacity), si bien qu'un thread qui le lit sait si l'élément est utilisable sans aucun verrou.

03

Ce que le numéro de séquence apporte

Pour mesurer sa valeur, retirons-le. Le consommateur négligent ci-dessous réserve une position avec le comparer-et-échanger puis lit aussitôt la case, sans regarder seq. Contre un producteur d'un seul élément, il a 20 ordonnancements, et dans 10 d'entre eux il réserve une position avant que le producteur ait rien publié et renvoie None comme élément, ou le mauvais élément. La vraie file a 4 ordonnancements pour le même programme et aucun ne casse. Le numéro de séquence est exactement le contrôle qui transforme un ticket réservé en élément livré.

main.pyPython
def careless_dequeue(shared, capacity):    yield    pos = shared["deq"]    cell = pos % capacity    yield    if not compare_and_swap(shared, "deq", pos, pos + 1):        return None    yield    item = shared.get("data%d" % cell)    shared["got%d" % pos] = item    return itemdef careless_consumer(attempts, capacity):    def thread(shared):        for _ in range(attempts):            yield from careless_dequeue(shared, capacity)    return threadmake = lambda shared: [producer_thread((7,), 2)(shared), careless_consumer(1, 2)(shared)]outcomes = explore_all(make, init_queue(2))broken = sum(n for key, n in outcomes.items() if violations(dict(key)))print(sum(outcomes.values()), "schedules,", broken, "break the invariants")
20 schedules, 10 break the invariants
La capacité doit être d'au moins deux

Avec une seule case, la valeur p + 1 qui signifie « prête pour la position p » est aussi celle qui signifie « libre pour la position p + 1 », et un producteur pourrait écraser un élément qui n'a pas été lu. C'est pourquoi les vraies implémentations arrondissent la capacité à une puissance de deux, au moins 2, ce qui transforme aussi % capacity en un masque bon marché.

main.pyPython
ticks = [int(round(float(c) * 100)) for c in load_bars()["close"].iloc[:2]]print(ticks)print("two producers:", verify([(ticks[0],), (ticks[1],)], [2], 2))print("two consumers:", verify([tuple(ticks)], [1, 1], 2))
[9369, 9305]
two producers: {'schedules': 2756, 'broken': {}, 'pushed': 2, 'popped': 2}
two consumers: {'schedules': 1600, 'broken': {}, 'pushed': 2, 'popped': 2}
Ce que disent les vraies donnéesles deux premiers cours de clôture SPY (9 369 et 9 305 cents) dans une file de deux cases
Deux producteurs, un consommateur2,756ordonnancements, 0 viole un invariant
Un producteur, deux consommateurs1,600ordonnancements, 0 viole un invariant
Consommateur négligent10des 20 ordonnancements lisent un élément absent

Les deux formes de la file sont justes sur tous les ordonnancements, l'explorateur en témoin, tandis qu'une version sans le contrôle de séquence échoue dans la moitié de son espace (bien plus petit). Producteurs et consommateurs ne s'attendent jamais : un consommateur qui ne trouve rien de prêt revient simplement, et un producteur qui trouve la file pleine renvoie False. C'est le prix et le cadeau d'une file sans verrou : les opérations peuvent échouer, mais ne bloquent jamais.

04

Ce que vous allez construire

Le labo suit le cours : la file vide avec ses numéros de séquence, enqueue et dequeue avec réservation par comparer-et-échanger et publication, la vérification des deux invariants, le vérificateur qui lance l'explorateur pour un nombre quelconque de producteurs et de consommateurs, et l'étude sur deux cours SPY.

  • Fonction · questionS'appuie sur
  • init_queue(capacity)À quoi ressemble une file vide ?De l'arithmétique simple
  • enqueue(shared, item, capacity)Comment un producteur réserve-t-il et publie-t-il ?De l'arithmétique simple
  • dequeue(shared, capacity)Comment un consommateur réserve-t-il et libère-t-il ?De l'arithmétique simple
  • violations(final)Quels invariants un état final viole-t-il ?De l'arithmétique simple
  • verify(producers, consumers, capacity)La file est-elle juste sous tous les ordonnancements ?explore_allviolations
  • study(bars)Les deux formes tiennent-elles sur les cours SPY ?verify

À retenir

  • Tickets par comparer-et-échanger, disponibilité par numéros de séquence. Réserver une position la rend vôtre ; le numéro de séquence dit à tous quand l'élément qu'elle contient est prêt.
  • Pas prêt n'est pas une erreur. Un consommateur qui ne trouve rien de prêt renvoie None et réessaie plus tard ; il n'attend jamais et ne lit jamais n'importe quoi.
  • Prouvez-le, en retirant le numéro de séquence comme témoin. 1 600 et 2 756 ordonnancements sans violation ; le consommateur négligent casse dans 10 sur 20.
Vérification rapide
Une file a 4 cases. Un consommateur a réservé la position 6 et libéré sa case. Quel est maintenant le numéro de séquence de cette case, et pour quelle position est-elle libre ?
Choisissez une réponse.

Sujet suivant : vous avez maintenant prouvé la justesse de petits programmes en exécutant tous les ordonnancements. Tester le code concurrent compare cela aux outils qu'utilisent les vraies équipes : un détecteur de courses qui observe une exécution et signale les accès qui pourraient se heurter, et un vérificateur de modèles qui fait ce que fait votre explorateur, mais avec les astuces qui lui permettent de passer à l'échelle.

Lab : ce qui est noté

6 tâches · 100 pts
  • La file vide5 pts
  • Enqueue25 pts
  • Dequeue25 pts
  • Les invariants10 pts
  • Vérifier sur tous les ordonnancements15 pts
  • L'étude sur la table SPY20 pts
Explorateur · lab-lf-08
▾ lab-lf-08
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-lf-08$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

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