σμ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 Academyロードマップスキル
Ctrl K
AO
+1.2k 人の学習者
ログイン参加する ↗
ロードマップスキル
AO
+1.2k 人の学習者
ログイン
  • 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 Academyロードマップスキル
Ctrl K
AO
+1.2k 人の学習者
ログイン参加する ↗
ロードマップスキル
AO
+1.2k 人の学習者
ログイン
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Lock-free スキル›モジュール 3 · ロックフリーキュー›トピック 3.1·~1 時間 30 分上級

単一プロデューサ・単一コンシューマのリングバッファ

スキル上の位置モジュール 3/4 · トピック 1/3
3.1 単一プロデューサ・単一コンシューマのリングバッファ
—ラボスコア— / 100合計XP—連続日数—

読了 7分コード例 4件

  1. 01位置ごとに書き手が1つの円
  2. 02全スケジュールで不変条件を確かめる
  3. 03壊れる条件:2つ目のプロデューサ
  4. 04これから作るもの

最も単純なロックフリーキューには、比較交換さえ要りません。各位置の書き手を1つだけにします。tail を書くのはプロデューサだけ、head を書くのはコンシューマだけです。相手側の位置を古い値で読んでも常に安全なので、リングバッファはロックなしで2つのスレッド間にティックを渡せます。探索器は291通りのスケジュールでそれを証明し、2つ目のプロデューサを加えると1,072通り中458通りで壊れることを示します。

01

位置ごとに書き手が1つの円

リングバッファは、増える一方の2つのカウンタを持ちます。プロデューサはスロット tail % capacity に書いてから tail を増やして公開し、コンシューマはスロット head % capacity を読んでから head を増やして解放します。tail - head == capacity なら満杯、head == tail なら空です。なぜロックなしで安全なのでしょう。tail を書くのはプロデューサだけなので、手元の変数にコピーを持てます。空きを調べるために head を読み、その値が古くても、古い head は本物より小さいので、プロデューサはバッファを実際より埋まっていると思い、せいぜい要素を捨てるだけで、未読のスロットを上書きすることはありません。コンシューマが読む古い tail も小さいので、せいぜいバッファが空に見えるだけです。これが単一ライターの原則です。

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}

プロデューサの3手番で最初のティックが入ります。head を読み、要素を slot0 に格納し、tail: 1 を公開します。コンシューマの3手番のあとは head: 1、popped: (7,) です。最後の行ではプロデューサが両方の要素を入れ(slot0: 7、slot1: 8)、コンシューマが最初の1つを取りました。スレッド1は試行が1回残っているのでまだ生きています。

02

全スケジュールで不変条件を確かめる

キューは何を約束すべきでしょうか。2つあります。取り出した要素は入れた要素と同じ順序で欠けなく並ぶこと。つまり popped は pushed の接頭辞です(FIFO性)。そして入れていないものは取り出されないこと:len(popped) <= len(pushed)。スレッドは操作を終える手番そのものの中で入れたもの・取ったものを記録するので、violations は任意の最終状態で両方を確かめられます。verify は explore_all で全スケジュールの下でプログラム全体を実行し、各不変条件を壊すスケジュールを数えます。全スケジュールで broken が空なら、そのサイズのプログラムについては証明です。

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}

2つの要素を入れるプロデューサを2スロットのバッファに置き、2回試行するコンシューマが読むと、スケジュールは101通りで、不変条件を壊すものはありません。スロットが1つだけの場合、コンシューマが遅いとプロデューサは要素を捨てなければなりませんが、それでも正しく、25通りで違反なしです。(7, 8) が入った後に取り出しが (8,) なら、予想どおり順序違反です。

定義単一ライターの原則

共有変数ごとに、それを書くスレッドはちょうど1つで、読むだけのスレッドはいくつでもよい。書き手が1つなら、その変数に競合はなく、古い値を見た読み手も、その古い値が安全側にしかずれないなら安全です。

03

壊れる条件:2つ目のプロデューサ

安全性の議論は1つの事実に頼っています。tail の書き手が1人であることです。2つ目のプロデューサを加えると、2つのスレッドがそれぞれ自分の tail の考えを持ちます。両方が同じ head を読み、同じスロットを計算し、2回目の書き込みが1回目を上書きします。探索器はそれを見つけます。2つのプロデューサ(各1ティック)の1,072通りのスケジュールのうち458通りで、popped が pushed の接頭辞になりません。対策はこのリングへのパッチではなく別の構造、複数プロデューサのキューで、後のトピックで比較交換を使って作ります。

main.pyPython
print(verify([(7,), (8,)], 2, 2))
{'schedules': 1072, 'broken': {'order': 458}, 'pushed': 2, 'popped': 1}
実機では2つの書き込みの順序が重要

このマシンはすべての操作を順番どおりに実行します。実際のプロセッサでは、プロデューサは tail をreleaseストアで公開し、コンシューマはacquireロードで読まなければなりません。そうしないと、コンシューマが要素より先に新しい tail を見てしまいます(前のトピックの古いデータ)。また head と tail は別のキャッシュラインに置きます。同じラインだと2つのスレッドがラインを奪い合います(フォルスシェアリング)。

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}
実データが示すものSPYの最初の4つの終値(9,369、9,305、9,354、9,349セント)をリングに通す
プロデューサ1つ291通りのスケジュール、不変条件違反は0
プロデューサ2つ1,072通りのスケジュールを探索
壊れた順序4582プロデューサのスケジュール中
壊れた割合43%2プロデューサのスケジュール中
判定別のスケジュール数
プロデューサ1つ:全部正しい291
プロデューサ2つ:正しい614
プロデューサ2つ:順序違反458

このレッスンは2つの数に尽きます。位置ごとに書き手が1人なら291通りで正しく、同じ位置に書き手が2人いると1,072通りの43%で誤ります。探索器は推測したのではなく、すべてのインターリーブを実行しました。

04

これから作るもの

ラボは講義の順に進みます。空のリング、プロデューサとコンシューマのスレッド、2つの不変条件の検査、探索器を動かす検証関数、そしてSPYの終値をプロデューサ1つと2つでリングに通す研究です。

  • 関数 · 問い前提
  • initial_state()空のリングはどんな状態か?単純な算術
  • ring_producer(items, capacity)プロデューサはどう入れるか?単純な算術
  • ring_consumer(attempts, capacity)コンシューマはどう取り出すか?単純な算術
  • violations(final)この状態はどの不変条件を壊すか?単純な算術
  • verify(producers, attempts, capacity)リングはすべてのスケジュールで正しいか?explore_allviolations
  • study(bars)2つ目のプロデューサで何が壊れるか?verify

重要ポイント

  • 位置ごとに書き手は1人。 tail を書くのはプロデューサだけ、head を書くのはコンシューマだけなので、比較交換は不要です。
  • 古い読み出しは安全側にずれる。 古い head ではバッファが実際より埋まって見え、古い tail では空いて見えます。せいぜい要素が捨てられるか、取り出しが空振りするだけです。
  • 契約を破れば探索器が見つける。 1つの tail に2つのプロデューサがいると、1,072通り中458通りで順序が壊れます。
クイックチェック
リングのスロットは4つです。プロデューサは合計9個を入れ、コンシューマは6個を取り出しました。リングには何個あり、次の要素はどのスロットに入りますか。
答えを選んでください。

次のトピック:単一ライターの技にも限界があり、比較交換にも限界があります。ABA問題とタグ付きポインタでは、比較交換でロックフリーなスタックを作り、下でスタックが変わっているのに交換が成功してしまうスケジュールを示し、バージョンタグで直します。

ラボ:採点される内容

6タスク · 100点
  • 空のリング5点
  • プロデューサ20点
  • コンシューマ20点
  • 不変条件10点
  • 全スケジュールで検証する15点
  • SPYの表での研究30点
エクスプローラー · lab-lf-06
▾ lab-lf-06
▾ data
…
bash · ブラウザ内
learner@quantcraft:~/lab-lf-06$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

このトピックをリセットしますか?

lab-lf-06 の内容はすべてスターターファイルに戻り、ターミナルも消去されます。現在の解答は失われ、元に戻せません。

リセットされるもの
  • main.py → スターターコード
  • ターミナル履歴とテスト出力
  • 未提出の変更
保持されるもの
  • ラボの最高スコア
  • XPと連続日数
  • 講義の進捗とメモ