- はじめに
- 1. 並行性バグが特別な理由 — 再現しない失敗
- 2. 0段階: 共有状態を消せるか
- 3. データ競合の定義とメモリモデル
- 4. 原子性境界を決める
- 5. ロック — 悲観 vs 楽観、そしてロック順序
- 6. 単一プロセスを超えたら: DBトランザクションと分散ロックの境界
- 7. キューとバックプレッシャー — 待ち行列をどこに立てるか
- 8. 並行コードをテストする方法
- 9. 失敗モードのカタログ
- クイズ: 理解度を確認しましょう
- おわりに
- 参考資料
- 関連記事
はじめに
このブログには並行性を扱った記事がすでに2本あります。並行性 vs 並列性は2つの概念の違いを整理した概念記事であり、分散ロックパターン比較はどのインフラでロックを実装するかを比較した記事です。本記事はその間の空白を埋めます。つまり、アプリケーションを設計する人が実際に踏む作業順序、共有状態を消せるなら消し、消せないなら範囲を狭め、それでも残るものを守ってテストする順序を扱います。
データベース分離レベルの内部動作(MVCC、スナップショット分離)は扱いません。「いつアプリケーションではなくストレージに任せるべきか」という境界線までを押さえ、その内側はデータベースカテゴリに譲ります。
一行にまとめるとこうです。並行性設計の大半はロックを上手にかける技術ではなく、ロックする対象を先に減らす技術です。
1. 並行性バグが特別な理由 — 再現しない失敗
1-1. 失敗が入力ではなくタイミングの関数である
通常のバグは入力の関数です。同じ入力なら同じ失敗が出るので、再現ケースと回帰テストを作れます。並行性バグはスケジューリングの関数です。同じ入力、同じコード、同じサーバなのに、スレッドがどの瞬間にプリエンプトされたかで結果が分かれます。再現ケースは確率的にしか存在しません。
1-2. 観測が現象を変える
ログを1行足すと問題が消える、という経験はよくあります。ログはI/Oであり、I/Oはスレッドを譲らせて競合窓の幅を変えるからです。デバッガはさらに極端で、ブレークポイントが事実上グローバルロックのように働きます。したがって並行性の問題は観測で絞り込みにくく、コードを読んで不変条件を検討する静的推論の比重がはるかに大きくなります。
1-3. テストの成功は不在の証拠ではない
競合窓が100ナノ秒なら、ローカルで1,000回回してもその窓に入る確率は低いままです。逆に毎秒数千回呼ばれる本番では1日以内に必ず入ります。並行性ではテスト結果よりも設計上の論証が先です。「このコードはなぜ安全か」を文章で書けなければならず、テストはその論証が崩れた瞬間を捕まえる網にすぎません。
1-4. 失敗が静かに残る
並行性バグは例外を投げて終わりません。残高は合っているのに取引履歴が1件欠けていたり、在庫は0なのに注文が2件通っていたりします。発見は数日後の照合作業で、そのときには原因時点のログがすでに消えています。実務のチェック質問は1つです。「この関数がまったく同じ瞬間に2回実行されたら何が壊れるか」
2. 0段階: 共有状態を消せるか
最初の一手はロックの選択ではなく、ロックする対象を減らすことです。コストの低い順に並べたはしごを示します。
0段階 共有しない 不変データ / 値のコピー / 純粋計算
1段階 所有権を移す 一度に1主体だけが所有し、メッセージで渡す
2段階 範囲を狭める リクエストスコープ・スレッドローカル・タスクローカル
3段階 キーで分ける 同じキーは常に同じワーカー/パーティションへ
4段階 残りだけロック 原子性境界の定義 → ロック → テスト
2-1. 不変データと範囲の縮小
読むだけのデータは何本のスレッドが触れても安全です。設定やルーティングテーブルのように、たまに変わり頻繁に読まれる値は、その場で書き換えるのではなく新しいオブジェクトを作って参照ごと差し替えます。差し替えは参照1つの書き込みで終わり、読んでいた側は以前のスナップショットを見続けます。ただしコレクションだけ不変で要素が可変なら、何も保証されません。
- リクエストスコープ: リクエストの間だけ生きるオブジェクトは共有されません。便利さのためにシングルトンのキャッシュに載せた瞬間、共有状態になります。
- スレッドローカル: トレースIDやトランザクションコンテキストに向きますが、スレッドプールで再利用されるときに前のリクエストの値が残る漏れが多いので、クリーンアップ経路が必須です。
- 所有権の移譲: バッファを満たして渡したあとは自分では触らないパターンです。規律だけで守られるので、レビューで明示的に確認する必要があります。
2-2. キーによるパーティショニング
もっとも実用的な縮小手法です。状態をグローバルに置かずキーで分け、同じキーは常に同じワーカーへ送ります。口座ごとの残高、注文ごとの状態機械のように自然キーがあるドメインによく合います。キーの内側ではロックなしで逐次処理になり、キーの間には競合がありません。
代償も明確です。ホットキーが生まれるとそのキーのスループットが全体のボトルネックになり、パーティション数を変えるリバランス中には同じキーが2つのワーカーに同時に付く区間が生まれます。その区間のためのリース確認と冪等処理が併せて必要です。
2-3. 論争: メッセージパッシング vs 共有メモリとロック
現場の意見が割れる地点です。どちらか一方が正解だと言い切る記事は疑ったほうがよいでしょう。
- メッセージパッシング側(アクター、CSP): 状態を1つの主体が独占し、他はメッセージでのみ依頼するため、データ競合が構造的に消えます。所有者が1つなので推論が容易です。
- 共有メモリとロック側: メッセージパッシングは競合を消すのではなく形を変えるだけだと見ます。要求順序の入れ替わり、メールボックスの無限増加、応答待ちのデッドロックはそのまま残ります。複数アクターにまたがる不変条件は、結局は調整プロトコルを手で実装させることになります。
- 実際の分岐軸: 不変条件が1つのエンティティに収まるか複数にまたがるか、ランタイムがどちらを低コストで提供するか、チームがスタックトレースでデバッグするかメッセージログでデバッグするか。
不変条件が1つのエンティティに収まるならメッセージパッシングが、複数のエンティティをまとめる必要があるならトランザクション境界を持つストレージが有利です。好みではなく不変条件の形に依存する問題です。
3. データ競合の定義とメモリモデル
3-1. 定義とDRF-SC
Goメモリモデルはデータ競合を "a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses" と定義し、プログラマの義務を "Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access" と明記します。
その見返りが重要です。同じ文書は "In the absence of data races, Go programs behave as if all the goroutines were multiplexed onto a single processor" と述べます。一般にDRF-SCと呼ばれる性質です。競合を1つも残さなければ、命令の並べ替えやキャッシュの可視性といったハードウェアの話を考えなくて済みます。逆に1つでも残せば、そのプログラムの挙動についてどんな直感も保証されません。
3-2. 「整数を1つ読むだけ」が危険な理由
同期なしの読み取りは「値が少し古いだけ」と言い訳されがちです。実際に起こりうる結果はもっと広いものです。コンパイラがループ内の読み取りをレジスタに引き上げると、更新を永遠に見られません。並べ替えのために、別のスレッドからはフラグが先に立ってデータが後から書かれたように見えることがあります。ワードサイズを超える値は半分だけ更新された状態で読まれることもあります。
// 例 — 同期なしのフラグは終了を保証しない
var done bool // 複数のゴルーチンが触れるが保護されていない
go func() { work(); done = true }()
for !done { // コンパイラが done をレジスタに載せうる
}
3-3. 原子的操作は原子性を保証しない
もっとも多い誤解です。原子的な型は1つの操作の原子性だけを保証します。操作をつなげれば、その間が開きます。
安全なもの counter.Add(1) 1つの原子的操作
危険なもの if counter.Load() < limit { 読みと書きの間が開いている
counter.Add(1)
}
check-then-act 競合と呼びます。ここから重要な区別が出てきます。データ競合がなくても競合状態は残ります。上のコードはすべてのアクセスが原子的なのでデータ競合はありませんが、「上限を超えない」という不変条件は壊れます。レース検出器は前者を捕まえ、後者は捕まえません。
レース検出器は実行された経路で実際に発生した競合しか捕まえないため、カバレッジの広い統合テストと一緒にCIで回すときに価値が出ます。
4. 原子性境界を決める
4-1. 不変条件をまず文章で書く
クリティカルセクションは行数ではなく不変条件の単位で決めます。まず守るべき文章を書きます。「残高は負になれない」「予約席数の合計は総座席数を超えない」「決済完了を経ずに配送中へは進めない」。次に各文章が一瞬でも壊れる区間を探せば、その区間全体が1つの境界です。
例 — 残高引き落としの原子性境界
├─ 現在の残高を読む ┐
├─ 出金額と比較する │ この4段階の間に他の主体が残高を
├─ 残高から差し引く │ 読んだり変えたりすると不変条件が壊れる
└─ 新しい残高を書く ┘
4-2. 境界を広げる・狭めるときの代償
狭すぎれば不変条件が壊れます。これは正しさの問題なので交渉の対象になりません。広すぎればスループットが落ちます。これは性能の問題なので測定して交渉できます。したがってまず広く取って正しさを確保し、測定でボトルネックが確認されてからのみ狭めます。逆にやると、正しさの欠陥を最適化という名前で植えることになります。
4-3. クリティカルセクションの中でやってはいけないこと
クリティカルセクションの長さは、そのまま他のすべての主体の待ち時間です。
- ネットワーク呼び出しとディスクI/O: 遅延が制御できません。外部APIが1つ遅くなると、そのロックに並んだ全スレッドが一緒に止まります。
- 別のロックの取得: ロックを握ったまま別のロックを取った瞬間にデッドロックの可能性が生まれます。避けられないなら5-3の順序規則が必須です。
- ユーザーコールバックの呼び出し: 中で何が起きるか分からず、再入して同じロックを取り直す事故が多発します。
パターンに縮めるとロックの中ではメモリだけ触り、計算とI/Oは外でやるとなります。必要なデータをロックの中でコピーして出て、外で処理し、反映が必要ならもう一度短くロックします。このとき2回目のロック時点では状態が変わっている可能性があるので、再検証が必要です。
5. ロック — 悲観 vs 楽観、そしてロック順序
5-1. 悲観ロックと楽観ロック
悲観ロックは先にロックしてから作業します。在庫の引き落としや座席割り当てのように競合が実際に多く、リトライがユーザーに見える作業に向きます。代償は待機、デッドロックの可能性、ロック保持時間が決めるスループット上限です。取得タイムアウトは選択ではなく必須です。無限待機は障害をデッドロックに変えて診断を難しくします。
楽観ロックはロックせずに進み、反映時点で「自分が読んだあとに変わっていないか」を確認します。
-- 例 — バージョン列を使う楽観ロック
UPDATE accounts
SET balance = balance - 1000,
version = version + 1
WHERE id = 42
AND version = 7; -- 影響行が0なら衝突 → リトライ
衝突がまれで読みが圧倒的に多いときに有利ですが、衝突が増えるとリトライが急増してかえって遅くなります。リトライ上限とバックオフが必要で、リトライ時には必ずデータを読み直さなければなりません。以前のスナップショットで再試行すると永遠に失敗します。
5-2. HTTP境界での同じ問題
クライアントが値を読み、ユーザーが編集する間に別のユーザーがすでに書き換えている状況を lost update 問題と呼びます。RFC 6585は428 Precondition Requiredを定義し、このコードが "the origin server requires the request to be conditional" を意味し、lost update 問題を防ぐためのものだと説明します。サーバは条件なしの更新を428で拒否し、クライアントは検証子を条件付きヘッダに載せて送り直します。
PUT /accounts/42 HTTP/1.1
If-Match: "v7"
# サーバ状態がすでに v8 なら 412 Precondition Failed
# 条件ヘッダがまったくなければ 428 Precondition Required
5-3. ロック順序 — デッドロック予防の唯一の実用規則
Aを握ったスレッドがBを待ち、Bを握ったスレッドがAを待てば、両方が永遠に止まります。実務で通用する予防策は1つだけです。すべてのロックにグローバルな順序を与え、常にその順序でのみ取得します。
ロック階層の例(上から下へのみ取得可能)
1. テナントロック 2. 口座ロック 3. 注文ロック 4. キャッシュシャードロック
規則: 3を握った状態で2を取ってはいけない。
同じ階層の中ではキーをソートした順に取得する。
同じ階層のロックを2つ取る必要があるなら(口座間振替が典型です)、キーをソートして小さいほうから取ります。この規則1つで循環待機が構造的に不可能になります。取得タイムアウトはデッドロックを永久停止ではなく失敗として表面化させますが、検知手段であって予防策ではありません。
5-4. 読み書きロックの落とし穴
読みが多ければ有利に見えますが、実測なしに導入すると損になることが多いものです。クリティカルセクションが短ければ付随コストのために相互排他ロックより遅くなり、読みが途切れなければ書きが押し出される飢餓が起きます。読みロックを握ったまま書きロックへ昇格しようとするコードは代表的なデッドロックパターンです。
6. 単一プロセスを超えたら: DBトランザクションと分散ロックの境界
プロセスが1つから2つになった瞬間、言語が提供するロックは何も保証しません。インスタンスがそれぞれのミューテックスを取り、同じ行を同時に更新します。オートスケーリングを初めて有効にした日に表面化する代表的な事故です。
6-1. 順序: トランザクション → 条件付き更新 → 分散ロック
- 1つのデータベーストランザクションで終わるか。終わるならそこで終えます。検証済みの並行性制御がすでにストレージの中にあります。
- 制約や条件付き更新で表現できるか。ユニーク制約や
WHERE version = ?形式の条件付き更新は、ロックなしで正しさを作ります。「重複作成の防止」はたいていロックではなくユニーク制約の問題です。 - 複数のストレージや外部システムにまたがるか。そこで初めて分散ロックが登場します。
3番へ直行する設計はよくありますが、大半は1番か2番で終わります。
6-2. 分散ロックが保証しないもの
分散ロックは相互排他を保証しません。確率を下げるだけです。ロックはたいていTTLのリースなので、作業がTTLより長くかかると期限が切れて別のインスタンスが同じロックを取り、元のインスタンスは自分が握っていると信じて書き続けます。GCの一時停止やコンテナのスロットリングで数秒止まっても結果は同じで、アプリケーションコードからは検知する手段がありません。TTLの計算がノードごとの時計に依存すると、時計のドリフトがそのまま正しさの問題になります。
したがって分散ロックは正しさの最終防衛線ではなく、競合を減らす手段です。最終防衛線はストレージ側に置きます。更新時点でバージョンを検査する条件付き更新か、2回入っても結果が同じになる冪等設計です。
6-3. 冪等性は並行性設計の一部である
リトライのあるシステムは結局、同じ作業を2回実行します。RFC 9110は冪等性を "the intended effect on the server of multiple identical requests is the same as for a single request" と定義し、GET・HEAD・PUT・DELETE・OPTIONS・TRACEを冪等メソッドと規定します。POSTは冪等ではありません。したがってPOSTで作る資源には別途の冪等キーが必要です。
- クライアントがリクエストごとに一意な冪等キーを作って送ります。
- サーバはそのキーで結果を保存し、同じキーが再び来たら保存済みの結果をそのまま返します。
- キーの保存は実際の作業と同じトランザクションの中で行われなければなりません。そうでなければ、その間の失敗で重複が残ります。
- 同じキーが同時に2回届きうるので、キーテーブルのユニーク制約が実質的な防衛線です。
手順は冪等性とリトライにより詳しく整理されています。
7. キューとバックプレッシャー — 待ち行列をどこに立てるか
7-1. キューはすでに何層にも存在する
キューを置くかどうかを悩む前に認めるべき事実があります。ソケット受信バッファ、スレッドプールの作業キュー、コネクションプールの待ち行列、データベースのロック待ち行列は、すべてキューです。設計問題は「キューを置くか」ではなく、どのキューが先に限界に達し、そのとき何をするかです。
リクエスト → [ソケットバッファ] → [スレッドプールキュー] → [プール待ち] → [DBロック待ち] → 処理
各段階に上限と拒否ポリシーが必要。
上限のない段階が1つでもあれば、そこが遅延とメモリの穴になる。
7-2. 無限キューは障害を遅延時間に変える
上限がなければ、過負荷は失敗ではなく遅延の増加として現れます。問題は、その遅延がすでに意味を失ったリクエストにまで付くことです。クライアントは30秒前にタイムアウトしてリトライしているのに、サーバは誰も待っていないリクエストを処理し続けます。
Google SRE本の連鎖障害の章はこの構造を "a failure that grows over time as a result of positive feedback" と定義します。過負荷がリトライを生み、リトライが過負荷を大きくする正のフィードバックです。同じ章は多層リトライの掛け算効果も指摘します。3層がそれぞれ4回ずつリトライすれば、ユーザー操作1回が64回の試行になります。
7-3. 有限キューと拒否ポリシー
- 上限を決める: キュー長の上限はおおよそ「許容待ち時間 ÷ 平均処理時間」です。目標が200ミリ秒で処理に20ミリ秒かかるワーカーが10本なら、上限はおよそ100です。
- 早く拒否する: SRE本は無制限のキューイングではなく早期の拒否(たとえば503を返す)を勧めます。拒否は失敗ではなく保護です。
- 古い項目を捨てる: キュー投入時刻を記録し、クライアントのタイムアウトを超えた項目は取り出した瞬間に破棄します。
- リトライを制御する: SRE本は "Always use randomized exponential backoff when scheduling retries" と明記し、プロセスあたり毎分60回といったリトライ予算の例を挙げ、"Don't retry a given request indefinitely" と述べます。永続的なエラーとリトライ可能なエラーはコードで区別し、永続的なエラーは決してリトライしません。
- 逆方向へ信号を送る: 本当のバックプレッシャーは拒否ではなく生産者を遅くすることです。有限バッファへのブロッキング書き込みや、クレジットベースのフロー制御がこれに当たります。
リトライ方針が成功確率に与える影響はリトライ確率計算機でシミュレーションできます。
7-4. 論争: async/await vs スレッド
実行モデルの選択にも合意された正解はありません。
- 非同期側: 接続ごとにスレッドを占有しないため、I/O待ちの多いワークロードではメモリとコンテキストスイッチのコストがはるかに低くなります。数万の同時接続が目標なら選択肢は狭まります。
- スレッド側: スタックトレースが無傷で、デバッガがそのまま動き、ブロッキングのライブラリをそのまま使えます。非同期は関数の色問題を作ってコードベース全体に伝染し、イベントループでCPUを長く使う1行が全体を止めます。
- 実際の分岐軸: ワークロードがI/O中心かCPU中心か、ランタイムが軽量スレッドを提供するか、依存エコシステムがどちら側か、チームのデバッグ道具がどちらに整っているか。
重要なのは、どちらのモデルも共有状態の問題を消してくれないことです。単一スレッドのイベントループでも await の地点で別のタスクが割り込みうるので、await を挟んだ check-then-act は依然として壊れます。実行モデルを変えても2章から5章までの作業はそのまま残ります。
8. 並行コードをテストする方法
8-1. 決定的にできる部分を切り離す
もっとも効果的な手法は「同時に走らせて運を祈る」テストを減らすことです。状態遷移ロジックを純粋関数として抜き出せば、その部分は決定的にテストでき、残る並行性の表面は薄くなります。
8-2. 競合窓を人為的に広げる
ランダムな実行に頼るのではなく、望むインターリービングを強制します。バリアやラッチをテスト専用のフックとして仕込み、2本のスレッドがちょうどその地点で出会うようにします。
例 — 2つの更新を必ず重ねさせるテスト
スレッドA: 読み ──────┐(バリア待ち)──────→ 書き
スレッドB: 読み ──────┘(バリア解除)──────→ 書き
期待: 2つ目の書きが衝突として拒否される。
確率ではなく構造で lost update を再現する。
8-3. 反復実行、レース検出器、プロパティベーステスト
並行性テストは反復回数を上げて回します。CIで1回、夜間ジョブで数百回という二重構成が現実的です。レース検出器は別ジョブで有効にし、統合テストと一緒に回します。ランダム性があるならシードをログに残します。再現の唯一の手がかりです。
並行操作の結果がいずれかの逐次実行順序で説明できるべきだという性質は、プロパティとして表現しやすいものです。ランダムな操作列を同時に走らせて逐次モデルと突き合わせれば、手書きのケースよりはるかに広いインターリービングを走査できます(プロパティベーステスト実践)。
8-4. フレーキーテストを無視しない
断続的な失敗を再実行で流す習慣は、並行性の欠陥を隠すもっともよくある経路です。フレーキーテストはたいてい2つのうちどちらかです。テストの書き方が悪いか、本物の競合を見つけたかです。どちらかを確かめる前にリトライ設定で覆わない規則が必要です。
9. 失敗モードのカタログ
| 失敗モード | 症状 | 典型的な原因 | 一次対応 |
|---|---|---|---|
| デッドロック | スレッドが永久停止、CPU 0% | ロック取得順序の不一致 | グローバルなロック順序、取得タイムアウト |
| ライブロック | CPUは忙しいが進捗なし | 衝突後に全員が同時に譲りリトライ | バックオフにジッタ、リトライ上限 |
| 飢餓 | 特定の作業だけ回らない | 読み優先ロック、優先度のないキュー | 公平性オプション、待ち時間による昇格 |
| lost update | 後の保存が先の保存を上書き | 読み-変更-書きの間に保護がない | バージョン検査の条件付き更新、If-Match |
| 二重処理 | 決済やメールが2回出る | リトライと非冪等な操作 | 冪等キー、ユニーク制約 |
| 幻の応答 | 応答が別のリクエストの結果 | リクエストスコープの値を共有物に保存 | コンテキスト伝播の点検、スレッドローカルの掃除 |
| スレッドプール枯渇 | 全面的な遅延、ヘルスチェック失敗 | プールの中から同じプールを待つ | プールの分離、ブロッキング呼び出しの隔離 |
| コネクションプール枯渇 | DB待ちの急増 | トランザクションの中で外部呼び出し | クリティカルセクションからI/Oを除去 |
| キャッシュスタンピード | 期限切れ直後にオリジンへ殺到 | 同一キーが同時に期限切れ | 期限にジッタ、シングルフライト更新 |
| 順序逆転 | イベントが入れ替わって到着 | キーなしの並列消費 | キーによるパーティショニング、バージョンで破棄 |
繰り返されるパターンが見えます。項目の大半は共有範囲を減らしていればそもそも存在しなかった問題です。2章のはしごをもう1段降りるほうが、この表を暗記するより値打ちがあります。遅延の急増が見えるなら、コネクションプールのサイズとサーキットブレーカーパターンも併せて点検してください。
クイズ: 理解度を確認しましょう
クイズ1: 共有変数をすべて原子的な型にしたのに在庫がマイナスになります。何を誤ったのか
正解: 原子的操作は1つの操作の原子性しか保証しません。在庫を読んで検査してから差し引くのは2つの操作なので、その間が開いています。
説明: データ競合と競合状態は別の問題です。アクセスがすべて原子的ならデータ競合は消えますが、「在庫は0未満になれない」という不変条件は検査と差し引きを1かたまりにして初めて守られます。条件付き原子操作でまとめるか、1つのクリティカルセクションまたは1つの条件付き更新文にまとめる必要があります。レース検出器が静かだという事実は正しさの証拠になりません。
クイズ2: インスタンスを1台から4台に増やした日から重複注文が出ます。まず何を確認するか
正解: 重複防止のロジックがプロセス内のロックやインメモリキャッシュに依存していないかを確認します。
説明: 単一インスタンスでは言語のミューテックスが事実上グローバルな調停役を果たしていました。インスタンスが増えるとそれぞれが自分のミューテックスを取るので、その保証はまるごと失われます。対応は分散ロックを先に導入することではなく、ストレージ側にユニーク制約か条件付き更新を置くことです。分散ロックはリースの期限切れとプロセス停止のため相互排他を保証できません。
クイズ3: ロック競合が激しくクリティカルセクションを縮めたい。まず外に出すものは何か
正解: クリティカルセクション内のネットワーク呼び出しとディスクI/Oです。
説明: クリティカルセクションの長さは他のすべての待機者の待ち時間であり、I/Oはそのうち制御できない部分です。外部APIが10ミリ秒から2秒に遅くなると、そのロックに並んだ全スレッドが2秒ずつ押し出されます。データをロックの中でコピーして出て、外でI/Oを行い、反映するときにもう一度短くロックします。2回目のロック時点では状態が変わっている可能性があるので再検証が必要です。
クイズ4: 過負荷時に応答時間が30秒まで伸びるがエラー率は0%です。良い状態か
正解: いいえ。上限のないキューがあるという合図であり、すでに誰も待っていないリクエストを処理している可能性が高い状態です。
説明: エラー率0%と応答30秒の組み合わせは、過負荷を拒否せず積んでいるという意味です。クライアントはその前にタイムアウトしてリトライするので、サーバは捨ててよい作業を処理しながら新しいリクエストまで押し出します。キューに上限を置いて超過分を即座に拒否し、投入時刻を記録してタイムアウトを過ぎた項目は取り出した瞬間に捨てなければなりません。
クイズ5: 並行性テストが500回に1回失敗します。次の行動として適切なものは何か
正解: 再実行で流さず、テスト自体の欠陥か実際の競合かを判定するのが先です。
説明: 断続的な失敗は、確率の低いインターリービングでのみ現れる本物の欠陥の唯一の合図でありえます。失敗時のシードを記録し、バリアでそのインターリービングを強制して決定的なテストに変えてみます。再現すれば実際の欠陥であり、しなければテストのタイミング仮定が誤っています。判定の前にリトライ設定で覆えば、その合図は本番で再び現れます。
おわりに
並行性設計でもっとも多く使う道具はミューテックスではなく削除です。共有しなくてよいものを共有せず、グローバルでなくてよいものをグローバルに置かず、キーで分けられるものを分ければ、実際にロックすべき対象は驚くほど減ります。
残ったものは順序を守れば済みます。不変条件を文章で書き、その文章が壊れる区間を原子性境界とし、境界の中ではメモリだけを触り、プロセスの外に出たらストレージの制約と冪等性を最終防衛線に置き、キューごとに上限と拒否ポリシーを付けます。「たぶん大丈夫でしょう」は設計ではありません。なぜ安全かを一段落で書けないなら、そのコードはまだ完成していません。
参考資料
- The Go Memory Model — go.dev — データ競合の定義、同時アクセスを直列化する義務、競合がないときの逐次一貫性(DRF-SC)の一文を引用しました。2026-08-15確認。
- RFC 6585: Additional HTTP Status Codes — IETF — 428 Precondition Required が条件付きリクエストを要求し lost update 問題を防ぐという説明を引用しました。2026-08-15確認。
- RFC 9110: HTTP Semantics — IETF — 冪等性の定義と冪等メソッドの一覧、POSTが冪等でない点を引用しました。2026-08-15確認。
- Google SRE Book — Addressing Cascading Failures — 連鎖障害の定義、ランダム化指数バックオフの推奨、リトライ予算の例、多層リトライの掛け算効果、早期拒否の推奨を引用しました。2026-08-15確認。
- 共有状態の縮小はしご、ロック階層の例、9章の失敗モード表は上記資料に出てくるものではなく、本記事で整理した手順です。
関連記事
- このブログの関連記事: 並行性 vs 並列性
- このブログの関連記事: 分散ロックパターン比較
- このブログの関連記事: 冪等性とリトライ: 信頼できるAPI
- このブログの関連記事: コネクションプールのサイズを大きくすると損な理由
- 関連ツール: リトライ確率計算機
- 関連ツール: レートリミットシミュレータ
完全ガイドシリーズ
현재 단락 (1/149)
このブログには並行性を扱った記事がすでに2本あります。[並行性 vs 並列性](/blog/2026-07-03-concurrency-vs-parallelism)は2つの概念の違いを整理した概念...