プロデューサ1つ・コンシューマ1つなら、位置の書き手は1つです。多数になると位置をアトミックに配る必要があり、チケットだけでは足りません。位置5を持つプロデューサはまだ要素を書いていないからです。各セルの連番が、そのセルが誰の番かを示します。プロデューサ2つの2,756通りとコンシューマ2つの1,600通りのスケジュールで、このキューは一度も壊れません。
チケットは配達ではない
capacity 個のセルを持つ有界キューは、増える一方の2つのカウンタを使います。プロデューサは enq への比較交換で次の位置を取り、コンシューマは deq への比較交換で次の位置を取ります。位置 p はセル p % capacity にあります。比較交換により予約は安全になり、2つのプロデューサが同じ位置を得ることはありません。しかし位置の予約は要素の書き込みではありません。交換と格納の間、セルは予約済みで空であり、いま読んだコンシューマはゴミを読みます。そこで各セルは、正しい瞬間にだけ進む連番を持ちます。
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}3つの実行を読みましょう。[0, 0, 0] のあと、プロデューサは位置0を予約し(enq: 1)、data0 を格納し、seq0 を1に上げたので要素は準備完了です。コンシューマの3手番のあとは got0: 7、seq0: 2 で、セルは次の周回のために空きます。最後の実行では、コンシューマがプロデューサの予約と公開の間にのぞき、seq0 が1ではなく0なのを見て、手ぶらで去ります。ゴミは読みません。「まだ準備できていない」と言うだけで、要素は後の試行で見つかります。
全スケジュールの検査
キューは今度は何を約束すべきでしょうか。プロデューサは各要素を位置ごとに記録し(put3)、コンシューマは取った要素を予約した位置ごとに記録します(got3)。キューが正しければ、取り出された各位置にはそこに入れた要素があり(mismatch なし)、取り出された位置は 0, 1, 2, ... と穴も重複もなく並びます(gap なし)。これがキューの順序、つまり位置の順序です。violations は最終状態からこの2つの事実を読み、verify は任意の数のプロデューサとコンシューマで探索器を動かします。ここでは、2要素の1プロデューサと2コンシューマ、そして1要素と1コンシューマです。
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}各セルが持つ、誰の番かを記録するカウンタ。要素が書かれたとき(p + 1 へ)と取られたとき(p + capacity へ)にしか変わらないので、これを読んだスレッドは、ロックなしでその要素が使ってよいかどうかを知れます。
連番が何をもたらすか
価値を見るために、取り除いてみましょう。下の不注意なコンシューマは、比較交換で位置を予約したあと、seq を見ずにすぐセルを読みます。1要素のプロデューサに対して20通りのスケジュールがあり、そのうち10通りでは、プロデューサがまだ何も公開しないうちに位置を予約し、要素として None か誤った要素を返します。本物のキューは同じプログラムで4通りで、壊れるものはありません。連番は、予約したチケットを配達済みの要素に変える、まさにそのチェックです。
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
セルが1つだと、「位置pの準備完了」を意味する値 p + 1 が「位置 p + 1 のために空き」を意味する値と同じになり、プロデューサが未読の要素を上書きしかねません。そのため実際の実装は容量を2のべき乗(最低2)に切り上げ、% capacity を安価なマスクにもします。
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}キューの両方の形は、探索器を証人として、すべてのスケジュールで正しい一方、連番の検査を外した版は(ずっと小さい)空間の半分で失敗します。プロデューサとコンシューマは互いを待ちません。準備ができたものが見つからないコンシューマは単に戻り、満杯のキューを見つけたプロデューサは False を返します。これがロックフリーキューの代償であり贈り物です。操作は失敗しうるが、決してブロックしません。
これから作るもの
ラボは講義の順に進みます。連番付きの空のキュー、比較交換による予約と公開を行うenqueueとdequeue、2つの不変条件の検査、任意の数のプロデューサとコンシューマで探索器を動かす検証関数、そしてSPYの終値2つでの研究です。
- 関数 · 問い前提
- init_queue(capacity)空のキューはどんな形か?単純な算術
- enqueue(shared, item, capacity)プロデューサはどう予約し公開するか?単純な算術
- dequeue(shared, capacity)コンシューマはどう予約し解放するか?単純な算術
- violations(final)最終状態はどの不変条件を壊すか?単純な算術
- verify(producers, consumers, capacity)キューはすべてのスケジュールで正しいか?explore_allviolations
- study(bars)両方の形はSPYの終値で成り立つか?verify
次のトピック:ここまでは、全スケジュールを実行して小さなプログラムの正しさを証明してきました。並行コードのテストでは、それを実際のチームが使う道具と比べます。1回の実行を観察して競合しうるアクセスを指摘するレース検出器と、あなたの探索器と同じことを、規模を広げる工夫付きで行うモデル検査器です。
ラボ:採点される内容
6タスク · 100点- 空のキュー5点
- enqueue25点
- dequeue25点
- 不変条件10点
- 全スケジュールで検証する15点
- SPYの表での研究20点