並列処理のコードを前に、同時に動いたときのことを頭の中で追いかけている開発者

AIに非同期・並行処理を書かせる|競合とデッドロックの確認

「並列で処理して速くして」とAIに頼んだら、数十行できれいなコードが返ってきた。動かしてみると、ちゃんと速い。テストも通る。——ここまでは、たぶん今日もどこかで起きています。

そして問題が出るのは、たいてい数週間あとです。しかも「たまに」しか出ない。再現しようとしても再現しない。ログを見ても、どの処理も成功したことになっている。

このしんどさは、あなたの確認が甘かったから起きるのではありません。並行処理の不具合は、順番に動かしているあいだは絶対に姿を見せないという性質を持っているだけです。つまり、普段のやり方で見つからないのは当たり前なのです。この記事では、そこをAIとどう分担するかを整理します。

結論:AIに非同期・並行処理を書かせるときの要点は3つです。①コードを頼む前に「同時に来るもの」を3行書く——何が共有されるか/同時に起こりうる操作は何か/絶対に崩したくない条件は何か。②「動くコードを書いて」ではなく「このコードが壊れる筋書きを、時系列で書いて」と頼む。AIは正しい実装より、壊れ方の説明のほうが得意です。③直す前に、壊れることを確かめる仕掛けを1つだけ入れる——競合検出のオプション、同時実行の再現テスト、DBの一意制約のどれか1つで十分です。

AIが苦手なのは、あなたのシステムで「何が同時に起こるか」を知ることです。そこだけこちらが渡せば、あとはかなり頼れます。

全部を今日やる必要はありません。まずは①の3行からで大丈夫です。

何が起きているのか——順番に動かすと、絶対に出てこない

ひとりずつ順番に札を取る場面、二人が同時に同じ札に手を伸ばす場面、お互いの札を待って動けない場面を並べた図
順番に来れば起きないことが、同じ瞬間に重なると起きる。そして、お互いの完了を待ち合うと、そのまま止まります

まず、なぜここだけ勝手が違うのかを共有させてください。理由は3つあります。

1つ目は、不具合が「タイミング」でしか現れないこと。図の左と中央の違いです。ひとりずつ順番に棚の札を取りに来るなら、何も起きません。ところが二人が同じ瞬間に同じ1枚へ手を伸ばすと、両方が「取れた」と思ってしまう。この、同じものに同時に触ったせいで結果が変わってしまう状態競合状態(レースコンディション)と呼びます。手元での実行は、ほぼ必ず順番どおりに動きます。だから出ません。出るのは、アクセスが増えた日だけです。

2つ目は、お互いの完了を待ち合うと止まってしまうこと。図の右です。二人がそれぞれ別のものを確保したまま、相手が持っているものを待つ。どちらも譲らないので、永久に進みません。これがデッドロックです。厄介なのは、エラーにならないところ。落ちてくれれば気づけるのに、ただ静かに止まるので、監視にも「処理中」としか出ません。

3つ目は、AIがこの2つを「頼まれていない」こと。ここが本題です。「並列で速くして」と頼めば、AIは並列にします。同時アクセスを想定してほしいとは、こちらが一言も言っていないのです。AIは目の前のコードしか見ていないので、そのメソッドが1日1回のバッチから呼ばれるのか、秒間数百のリクエストから呼ばれるのかを知りません。知らないまま、いちばん素直な——つまり同時実行を考えない——コードを書きます。手を抜いたわけではなく、判断材料を渡していなかっただけです。

この3つが重なると、「レビューも通り、テストも通り、本番で月に一度だけおかしくなる」という、いちばん追いにくい状態ができあがります。逆に言えば、「何が同時に来るか」を先に言葉にして渡すだけで、かなりの部分は入口で防げるということでもあります。

具体例——AIの非同期コードが、こう抜ける

現場で実際に見かけやすいのは、次のような形です。一つでも心当たりがあれば、この記事はそのまま使えます。

どれも、注意力の問題ではありません。これらは「動いてしまう」ことが特徴なので、動かして確かめるという普段のやり方では見つからない、というだけです。見つけにくいものを相手にしている——そう捉えるほうが、実態に近いと思います。

影響——出るのは、いちばん忙しい日

並行処理の不具合が少し特別なのは、出るタイミングを選べないところです。

ただ、ここは強調しておきたいのですが、この種の抜けは、書く前なら本当に安く塞げます。あとから探すのは大変でも、先に「何が同時に来るか」を問うのは1分です。いま実装の手前でこの記事を読んでいるなら、それはいちばん安いタイミングにいるということです。

明日からやること(3ステップ)

大がかりな設計変更は要りません。この順番で、今日から小さく始められます。

1. AIに頼む前に、自分で3行書く

いきなり「並列にして」と頼まないほうが、返ってくるものの質が変わります。先に、次の3つを1行ずつ書きます。

①同時に触られるもの(どのデータ・どの行・どのファイル・どの外部サービス)
②同時に起こりうる操作(誰が・どこから・どれくらいの頻度で呼ぶか)
③崩したくない条件(在庫は0未満にならない/同じメールアドレスは1件だけ/通知は1回だけ)

例:

書いてみると分かるのですが、難しいのは③です。ここが埋まらないまま「速くして」と頼むと、AIは速さだけを最適化します。③は、AIが守るべき線をこちらから示す唯一の行だと思ってください。この3行が、そのままAIへ渡す素材になります。前提や制約の渡し方そのものはAIにコードを書かせる前に渡す前提と制約|伝え方の型と同じ考え方です。

2. 「壊れる筋書き」を時系列で書かせる

3行を渡したうえで、実装ではなく、壊れ方を先に聞きます。これはAIがかなり得意な仕事です。

そのまま使える頼み方の例:

以下のコードと前提について、修正案は出さなくて結構です。 このコードが同時に2回以上呼ばれたときに、結果がおかしくなる筋書きを挙げてください。それぞれ、処理Aと処理Bがどの順番で進むと壊れるのかを、時系列の表で示してください。とくに次の3点を名指しで見てください。

1. 確認してから書き込むまでのあいだに、割り込まれる余地はないか
2. 待ち合わせ(await・非同期呼び出し)をまたいだ前後で、前提が変わっていないか
3. 複数のロックや資源を取る順番が、呼び出し経路によって食い違っていないか

あわせて、リトライやタイムアウトで同じ処理が2回実行された場合に何が起きるかも書いてください。

前提:
(ここに3行)
コード:
(ここに対象のコード)

ポイントは、「修正案は出さなくて結構です」と先に伝えることです。これを言わないと、AIは指摘のあとに親切な修正コードを並べてくれて、そちらに目が行きます。いま欲しいのは、自分が見落としている順番の組み合わせです。

返ってきた筋書きは、そのまま信じずに1つずつ「これは実際に起こせるか」を確かめてください。AIは、実際には1つのプロセスからしか呼ばれない処理に、並行の心配を持ち出すこともあります。全部に対応する必要はありません。 起こせるものだけ拾えば十分です。指摘の選り分け方はAIレビューの指摘を取捨選択する|直す・直さないの判断軸が使えます。

直し方まで進むなら、選択肢はそう多くありません。同時に触られる範囲を狭くする(そもそも共有しない)、DB側で守る(一意制約、行ロック、更新時に版を確認する楽観ロック)、順番を1本にする(キューに並べて1つずつ処理する)。いちばん安いのは、たいてい2つ目のDB側で守る形です。アプリ側でどれだけ気をつけても、別の経路から更新されれば終わりですが、制約はどの経路にも効きます。SQLまわりの確認はAIに書かせたSQLの確認ポイント|N+1・インデックス漏れとあわせてどうぞ。

3. 「壊れること」を確かめる仕掛けを1つ入れる

直したあと、直ったことをどう確かめるかが最後の壁です。ここも1つだけで構いません。

なお、こうしたテストは通ったからといって「ない」ことの証明にはなりません。あくまで、よくある型を1つ潰したという話です。ここは正直に見ておいたほうが、あとで楽になります。AIレビューを通しても残るものについてはAIにレビューさせてもバグが残る|人が見るべき観点にも書きました。

3つとも今日やる必要はありません。ステップ1の3行を書くだけでも、AIから返ってくるコードは変わります。

非同期・並行処理のチェックリスト

全部に○が要るわけではありません。気になった行だけで十分です。

① AIに頼む前に

② 返ってきたコードで

③ 出す前に

この記事のまとめ

AIは非同期・並行処理のコードを、驚くほど速く、それらしく書きます。得意なのは書くことと、壊れ方を説明すること。苦手なのは、あなたのシステムで何が同時に起こるかを知ることです。それは、渡していない情報だからです。

やることは3つだけです。①「同時に触られるもの・同時に起こる操作・崩したくない条件」を3行書く/②「修正案は出さなくていいので、壊れる筋書きを時系列で書いて」と頼む/③壊れることを確かめる仕掛けを1つ入れる。この3つがあると、並行処理は「祈りながらリリースする作業」から、「決めて確かめる作業」に近づきます。

同時に動いたときの確認をひととおり終えて、安心した表情で顔を上げている開発者

並行処理を怖いと感じるのは、目に見えない順番を頭の中だけで追いかけているからだと思います。それを面倒がらずにやろうとしている時点で、かなり丁寧な仕事です。今日ひとつ「これ、同時に来たらどうなるだろう」と問えたなら、それだけで、いちばん忙しい日に鳴る電話をひとつ減らしています。

よければ、こちらも

関連用語