σμ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.1·~1 h 30 minAvancé

Un tampon circulaire à un producteur et un consommateur

Position dans le parcoursModule 3 sur 4 · Sujet 1 sur 3
3.1 Un tampon circulaire à un producteur et un consommateur
—Score du lab— / 100XP total—Série—

5 min de lecture4 exemples de code

  1. 01Un cercle avec un seul écrivain par position
  2. 02Vérifier les invariants sur tous les ordonnancements
  3. 03Ce qui le casse : un second producteur
  4. 04Ce que vous allez construire

La file sans verrou la plus simple n'a pas besoin de comparer-et-échanger. Donnez à chaque position un seul écrivain : le producteur seul écrit la queue, le consommateur seul écrit la tête. Une lecture périmée de la position de l'autre côté est toujours sûre, donc un tampon circulaire peut passer des ticks entre deux threads sans verrou. L'explorateur le prouve sur 291 ordonnancements, et le montre en défaut dans 458 des 1 072 dès qu'un second producteur s'ajoute.

01

Un cercle avec un seul écrivain par position

Un tampon circulaire garde deux compteurs qui ne font que croître. Le producteur écrit la case tail % capacity puis publie en augmentant tail ; le consommateur lit la case head % capacity puis la libère en augmentant head. Le tampon est plein quand tail - head == capacity et vide quand head == tail. Pourquoi est-ce sûr sans verrou ? Le producteur est le seul écrivain de tail, il peut donc en garder une copie locale. Il lit head pour chercher de la place, et si cette valeur est périmée, la tête périmée est plus petite que la vraie, donc le producteur croit le tampon plus plein qu'il ne l'est et, au pire, abandonne un élément, sans jamais écraser une case non lue. La tail périmée du consommateur est elle aussi plus petite, donc au pire il voit le tampon vide. C'est le principe de l'écrivain unique.

main.pyPython
def initial_state():    return {"head": 0, "tail": 0, "pushed": (), "popped": ()}def ring_producer(items, capacity):    def thread(shared):        tail = 0        for item in items:            yield            head = shared.get("head", 0)            if tail - head < capacity:                yield                shared["slot%d" % (tail % capacity)] = item                yield                tail += 1                shared["tail"] = tail                shared["pushed"] = shared.get("pushed", ()) + (item,)    return threaddef ring_consumer(attempts, capacity):    def thread(shared):        head = 0        for _ in range(attempts):            yield            tail = shared.get("tail", 0)            if head != tail:                yield                item = shared.get("slot%d" % (head % capacity))                yield                head += 1                shared["head"] = head                shared["popped"] = shared.get("popped", ()) + (item,)    return threadtwo = lambda shared: [ring_producer((7, 8), 2)(shared), ring_consumer(2, 2)(shared)]for prefix in ([0, 0, 0], [0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1, 1, 1]):    print(prefix, run_prefix(two, prefix, initial_state())[0])
[0, 0, 0] {'head': 0, 'tail': 1, 'pushed': (7,), 'popped': (), 'slot0': 7}
[0, 0, 0, 1, 1, 1] {'head': 1, 'tail': 1, 'pushed': (7,), 'popped': (7,), 'slot0': 7}
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1] {'head': 1, 'tail': 2, 'pushed': (7, 8), 'popped': (7,), 'slot0': 7, 'slot1': 8}

Trois tours du producteur poussent le premier tick : il lit la tête, range l'élément dans slot0 et publie tail: 1. Après trois tours du consommateur, head: 1 et popped: (7,). Sur la dernière ligne, le producteur a poussé les deux éléments (slot0: 7, slot1: 8) et le consommateur a pris le premier ; le thread 1 est encore vivant car il lui reste une tentative.

02

Vérifier les invariants sur tous les ordonnancements

Que doit promettre une file ? Deux choses. Les éléments sortis sont les éléments entrés, dans le même ordre et sans trou : popped est un préfixe de pushed (propriété FIFO). Et rien n'est sorti qui n'ait été poussé : len(popped) <= len(pushed). Les threads consignent ce qu'ils poussent et sortent au tour même qui termine l'opération, si bien que violations peut vérifier les deux sur n'importe quel état final. verify exécute alors tout le programme sous chaque ordonnancement avec explore_all et compte les ordonnancements qui violent chaque invariant. Un dictionnaire broken vide sur tous les ordonnancements est une preuve, pour cette taille de programme.

main.pyPython
def violations(final):    pushed, popped = tuple(final.get("pushed", ())), tuple(final.get("popped", ()))    broken = []    if popped != pushed[: len(popped)]:        broken.append("order")    if len(popped) > len(pushed):        broken.append("count")    return sorted(broken)def verify(producers, attempts, capacity):    def make_threads(shared):        return [ring_producer(items, capacity)(shared) for items in producers] + [ring_consumer(attempts, capacity)(shared)]    outcomes = explore_all(make_threads, initial_state())    broken = {}    pushed = popped = 0    for key, count in outcomes.items():        final = dict(key)        pushed = max(pushed, len(final.get("pushed", ())))        popped = max(popped, len(final.get("popped", ())))        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(violations({"pushed": (7, 8), "popped": (8,)}))print(verify([(7, 8)], 2, 2))print(verify([(7, 8)], 2, 1))
['order']
{'schedules': 101, 'broken': {}, 'pushed': 2, 'popped': 2}
{'schedules': 25, 'broken': {}, 'pushed': 2, 'popped': 2}

Un producteur de deux éléments dans un tampon de deux cases, lu par un consommateur à deux tentatives, a 101 ordonnancements et aucun ne viole d'invariant. Avec un tampon d'une seule case, le producteur doit abandonner un élément quand le consommateur est lent, mais il reste correct : 25 ordonnancements, aucune violation. La liste sortie (8,) après que (7, 8) a été poussé est bien une violation de l'ordre.

DéfinitionPrincipe de l'écrivain unique

Chaque variable partagée a exactement un thread qui l'écrit, et autant de threads que l'on veut qui la lisent seulement. Avec un seul écrivain il n'y a pas de course sur cette variable, et un lecteur qui voit une ancienne valeur reste en sécurité si l'ancienne valeur se trompe seulement du côté sûr.

03

Ce qui le casse : un second producteur

L'argument de sûreté repose sur un fait : tail a un seul écrivain. Ajoutez un second producteur et deux threads gardent chacun leur idée de la queue. Tous deux lisent le même head, calculent la même case, et la seconde écriture écrase la première. L'explorateur le trouve : 458 des 1 072 ordonnancements de deux producteurs (un tick chacun) finissent avec un popped qui n'est pas un préfixe de pushed. Le remède n'est pas un correctif de ce tampon mais une autre structure, une file multi-producteurs, que les sujets suivants bâtissent avec comparer-et-échanger.

main.pyPython
print(verify([(7,), (8,)], 2, 2))
{'schedules': 1072, 'broken': {'order': 458}, 'pushed': 2, 'popped': 1}
Sur du vrai matériel, l'ordre des deux écritures compte

Cette machine exécute chaque opération dans l'ordre. Sur un vrai processeur, le producteur doit publier tail par une écriture release et le consommateur doit le lire par un chargement acquire, sinon le consommateur peut voir la nouvelle queue avant l'élément (les données périmées du sujet précédent). Mettez aussi head et tail sur des lignes de cache différentes, sinon les deux threads se disputent une ligne (faux partage).

main.pyPython
ticks = [int(round(float(c) * 100)) for c in load_bars()["close"].iloc[:4]]print(ticks)print("one producer: ", verify([ticks[:2]], 3, 2))print("two producers:", verify([ticks[:1], ticks[1:2]], 2, 2))
[9369, 9305, 9354, 9349]
one producer:  {'schedules': 291, 'broken': {}, 'pushed': 2, 'popped': 2}
two producers: {'schedules': 1072, 'broken': {'order': 458}, 'pushed': 2, 'popped': 1}
Ce que disent les vraies donnéesles quatre premiers cours de clôture SPY (9 369, 9 305, 9 354 et 9 349 cents) poussés dans un tampon
Un producteur291ordonnancements, 0 viole un invariant
Deux producteurs1,072ordonnancements explorés
Ordre brisé458des ordonnancements à deux producteurs
Part en défaut43%des ordonnancements à deux producteurs
Ordonnancements selon le verdict
Un producteur : tous justes291
Deux producteurs : justes614
Deux producteurs : ordre brisé458

Toute la leçon tient en deux nombres : un écrivain par position suffit à la justesse sur 291 ordonnancements, et deux écrivains sur la même position se trompent dans 43 % de 1 072. L'explorateur n'a pas deviné ; il a exécuté tous les entrelacements.

04

Ce que vous allez construire

Le labo suit le cours : le tampon vide, les threads producteur et consommateur, la vérification des deux invariants, le vérificateur qui lance l'explorateur et l'étude qui envoie des cours de clôture SPY dans le tampon avec un puis deux producteurs.

  • Fonction · questionS'appuie sur
  • initial_state()À quoi ressemble un tampon vide ?De l'arithmétique simple
  • ring_producer(items, capacity)Comment le producteur pousse-t-il ?De l'arithmétique simple
  • ring_consumer(attempts, capacity)Comment le consommateur sort-il ?De l'arithmétique simple
  • violations(final)Quels invariants cet état viole-t-il ?De l'arithmétique simple
  • verify(producers, attempts, capacity)Le tampon est-il juste sous tous les ordonnancements ?explore_allviolations
  • study(bars)Qu'est-ce qui casse avec un second producteur ?verify

À retenir

  • Un écrivain par position. Seul le producteur écrit la queue et seul le consommateur la tête, donc aucun comparer-et-échanger n'est nécessaire.
  • Une lecture périmée se trompe du bon côté. Une tête périmée fait paraître le tampon plus plein et une queue périmée plus vide : au pire un élément est abandonné ou une sortie ne trouve rien.
  • Cassez le contrat et l'explorateur le trouve. Deux producteurs sur une seule queue brisent l'ordre dans 458 des 1 072 ordonnancements.
Vérification rapide
Un tampon a 4 cases. Le producteur a poussé 9 éléments au total et le consommateur en a sorti 6. Combien d'éléments le tampon contient-il, et dans quelle case sera rangé le prochain ?
Choisissez une réponse.

Sujet suivant : l'astuce de l'écrivain unique a une limite, et le comparer-et-échanger aussi. Le problème ABA et les pointeurs étiquetés bâtit une pile sans verrou sur comparer-et-échanger, montre un ordonnancement où l'échange réussit alors que la pile a changé en dessous, et le répare avec une étiquette de version.

Lab : ce qui est noté

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

Réinitialiser ce sujet ?

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