AIコードレビューの観点を整理したら、意外と知らない制御があった
Claude CodeやCopilotにコードレビューを任せるためのハーネスを構築していたときのことです。レビュー観点のチェックリストに「並行処理の制御」を追加しようとして、手が止まりました。
Mutex、デバウンス、Circuit Breakerあたりは書ける。でも「全部で何種類あるのか」と聞かれると、答えられない。
AIレビューに観点を教えるには、まず自分が全体像を把握していないと話にならない。そこで整理してみたら、17パターンもありました。しかも実務で意識していなかったものが結構ある。
この記事では、その17パターンを「レストラン運営」に例えて整理します。レストランも並行処理も、限られたリソースで複数の要求を同時にさばく必要がある点で同じです。
全体マップ:4つのカテゴリで整理する制御の世界
並行処理の制御パターンは、レストラン運営に例えると理解しやすくなります。
レストランも並行処理も、限られたリソース(厨房・スタッフ・席)を効率的に使って、複数の要求(お客様の注文)を同時に処理する必要があります。
4つのカテゴリに分類できます:
- 排他制御(誰が使えるか) - 厨房の使用権管理
- タイミング制御(いつ実行するか) - 注文タイミングの調整
- 協調制御(順番・合図) - スタッフ間の連携
- 安全制御(壊れた時どうするか) - トラブル時の対応
1. 排他制御:厨房の使用権を管理する
Mutex(ミューテックス):「厨房は一人ずつ」
一言: 同時に1つの処理だけを許可する仕組み
レストラン例: 狭い厨房は一度に1人のシェフしか使えない。2人入ったら包丁がぶつかります
いつ使う: ファイル書き込み、カウンター更新など
class Mutex {
private locked = false;
private waitingQueue: (() => void)[] = [];
async acquire(): Promise<void> {
return new Promise((resolve) => {
if (!this.locked) {
this.locked = true;
resolve();
} else {
this.waitingQueue.push(resolve);
}
});
}
release(): void {
if (this.waitingQueue.length > 0) {
const next = this.waitingQueue.shift()!;
next();
} else {
this.locked = false;
}
}
}
// 使用例:ファイル更新の排他制御
const fileMutex = new Mutex();
async function updateCounter() {
await fileMutex.acquire();
try {
// ファイル読み込み→更新→書き込み
const count = await readCounterFile();
await writeCounterFile(count + 1);
} finally {
fileMutex.release();
}
}
Semaphore(セマフォ):「厨房は3人まで」
一言: 指定した数まで同時処理を許可する仕組み
レストラン例: 大きな厨房は最大3人のシェフまで同時に作業可能。4人目は入口で待機
いつ使う: API同時接続数制限、ダウンロード並列数制御。Mutexの「上限1」を一般化したものです
class Semaphore {
private count: number;
private waitingQueue: (() => void)[] = [];
constructor(maxCount: number) {
this.count = maxCount;
}
async acquire(): Promise<void> {
return new Promise((resolve) => {
if (this.count > 0) {
this.count--;
resolve();
} else {
this.waitingQueue.push(resolve);
}
});
}
release(): void {
this.count++;
if (this.waitingQueue.length > 0) {
const next = this.waitingQueue.shift()!;
this.count--;
next();
}
}
}
// 使用例:API同時接続数制限
const apiSemaphore = new Semaphore(3);
async function callAPI(url: string) {
await apiSemaphore.acquire();
try {
return await fetch(url);
} finally {
apiSemaphore.release();
}
}
RWLock(読み書きロック):「レシピは見るだけOK、更新は1人ずつ」
一言: 読み込みは複数OK、書き込みは1つだけの制御
レストラン例: レシピ確認は何人でもOK、レシピ更新は1人ずつ
いつ使う: 設定値参照、キャッシュ読み書きなど
class RWLock {
private readers = 0;
private writer = false;
private waitingWriters: (() => void)[] = [];
private waitingReaders: (() => void)[] = [];
async acquireRead(): Promise<void> {
return new Promise((resolve) => {
if (!this.writer && this.waitingWriters.length === 0) {
this.readers++;
resolve();
} else {
this.waitingReaders.push(resolve);
}
});
}
async acquireWrite(): Promise<void> {
return new Promise((resolve) => {
if (!this.writer && this.readers === 0) {
this.writer = true;
resolve();
} else {
this.waitingWriters.push(resolve);
}
});
}
releaseRead(): void {
this.readers--;
if (this.readers === 0 && this.waitingWriters.length > 0) {
const next = this.waitingWriters.shift()!;
this.writer = true;
next();
}
}
releaseWrite(): void {
this.writer = false;
if (this.waitingWriters.length > 0) {
const next = this.waitingWriters.shift()!;
this.writer = true;
next();
} else if (this.waitingReaders.length > 0) {
this.waitingReaders.forEach(resolve => {
this.readers++;
resolve();
});
this.waitingReaders.length = 0;
}
}
}
Spinlock(スピンロック):「厨房の前で待機」
一言: 空きが出るまでひたすら確認し続ける仕組み
レストラン例: 厨房の前で「まだ?まだ?」と立って待つ。周りの迷惑です
いつ使う: OSカーネルやCPUレベルの超短時間排他制御向け。JavaScriptはシングルスレッドなので、while(true)で待つとブラウザが固まります。フロントエンドでは使わないでください
class Spinlock {
private locked = false;
acquire(): void {
while (this.locked) {
// ビジーウェイト(実際のJavaScriptでは非効率)
}
this.locked = true;
}
release(): void {
this.locked = false;
}
}
// 実用的なJavaScript版(非同期)
class AsyncSpinlock {
private locked = false;
async acquire(): Promise<void> {
while (this.locked) {
await new Promise(resolve => setTimeout(resolve, 1));
}
this.locked = true;
}
release(): void {
this.locked = false;
}
}
2. タイミング制御:注文タイミングを調整する
デバウンス:「注文確定は3秒待ってから」
一言: 連続実行を防ぎ、最後の実行から一定時間後に処理する
レストラン例: 「やっぱりパスタ」「いやカレー」「やっぱパスタ」。3秒黙ったら確定です
いつ使う: 検索入力、リサイズイベント。これがないとキーを打つたびにAPIが飛びます
function debounce<T extends (...args: any[]) => void>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: number | null = null;
return (...args: Parameters<T>) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => func(...args), delay);
};
}
// 使用例:検索入力
const debouncedSearch = debounce((query: string) => {
console.log(`検索実行: ${query}`);
// API呼び出し
}, 300);
// input要素での使用
document.getElementById('search')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
debouncedSearch(target.value);
});
スロットル:「注文は1分に1回まで」
一言: 一定間隔でのみ処理を実行する
レストラン例: どんなに「まだ?」と言われても、1分に1回しか注文を受け付けない。せっかちなお客様対策
いつ使う: スクロールイベント、ボタン連打防止。デバウンスとの違いは「最初の1回を即実行する」かどうか
function throttle<T extends (...args: any[]) => void>(
func: T,
interval: number
): (...args: Parameters<T>) => void {
let lastCallTime = 0;
return (...args: Parameters<T>) => {
const now = Date.now();
if (now - lastCallTime >= interval) {
lastCallTime = now;
func(...args);
}
};
}
// 使用例:スクロールイベント
const throttledScroll = throttle(() => {
console.log('スクロール位置更新');
updateScrollPosition();
}, 100);
window.addEventListener('scroll', throttledScroll);
Rate Limit:「1時間に10回まで」
一言: 指定時間内の実行回数を制限する
レストラン例: ランチタイムは1人1時間に10皿までの制限
いつ使う: API呼び出し制限、スパム防止など
class RateLimiter {
private requests: number[] = [];
constructor(
private maxRequests: number,
private windowMs: number
) {}
canExecute(): boolean {
const now = Date.now();
// 時間窓の外のリクエストを削除
this.requests = this.requests.filter(time => now - time < this.windowMs);
if (this.requests.length < this.maxRequests) {
this.requests.push(now);
return true;
}
return false;
}
getRetryAfter(): number {
if (this.requests.length === 0) return 0;
const oldestRequest = Math.min(...this.requests);
return this.windowMs - (Date.now() - oldestRequest);
}
}
// 使用例:API呼び出し制限
const rateLimiter = new RateLimiter(10, 60000); // 1分間に10回
async function callAPI() {
if (rateLimiter.canExecute()) {
return await fetch('/api/data');
} else {
const retryAfter = rateLimiter.getRetryAfter();
throw new Error(`Rate limit exceeded. Retry after ${retryAfter}ms`);
}
}
3. 協調制御:スタッフ間で連携する
Barrier(バリア):「全員準備できたら一斉スタート」
一言: 全ての処理が完了するまで待機し、一斉に次へ進む
レストラン例: コース料理は全ての皿が揃ってから一斉にサーブ。1皿だけ先に出したら「なんで俺のだけ?」になります
いつ使う: 並列処理の同期。Promise.all がまさにこれ
class Barrier {
private count = 0;
private waitingPromises: (() => void)[] = [];
constructor(private totalCount: number) {}
async wait(): Promise<void> {
return new Promise((resolve) => {
this.count++;
this.waitingPromises.push(resolve);
if (this.count === this.totalCount) {
// 全員準備完了、一斉に解放
this.waitingPromises.forEach(resolve => resolve());
this.waitingPromises = [];
this.count = 0;
}
});
}
}
// 使用例:並列データ処理の同期
async function processDataInParallel(data: any[]) {
const barrier = new Barrier(data.length);
const tasks = data.map(async (item) => {
const result = await processItem(item);
await barrier.wait(); // 全員の完了を待つ
return result;
});
return Promise.all(tasks);
}
Latch(ラッチ):「シェフの準備OKサインで一斉開始」
一言: 特定のイベントをトリガーとして複数の処理を開始
レストラン例: シェフの「準備OK」の合図で、ホールスタッフが一斉にサーブ開始
いつ使う: 初期化完了待ち、設定読み込み後の処理開始など
class CountDownLatch {
private count: number;
private waitingPromises: (() => void)[] = [];
constructor(count: number) {
this.count = count;
}
countDown(): void {
this.count--;
if (this.count === 0) {
this.waitingPromises.forEach(resolve => resolve());
this.waitingPromises = [];
}
}
async wait(): Promise<void> {
if (this.count === 0) return;
return new Promise((resolve) => {
this.waitingPromises.push(resolve);
});
}
}
// 使用例:初期化処理の完了待ち
const initLatch = new CountDownLatch(3);
async function initializeApp() {
// 3つの初期化処理を並列実行
loadConfig().then(() => initLatch.countDown());
connectDatabase().then(() => initLatch.countDown());
setupAuth().then(() => initLatch.countDown());
// 全ての初期化完了を待つ
await initLatch.wait();
console.log('アプリケーション初期化完了');
}
Event・Signal:「ベルを鳴らして知らせる」
一言: 特定のイベントの発生を他の処理に通知する仕組み
レストラン例: 注文完了のベルで、ホールスタッフに料理の準備完了を知らせる
いつ使う: 状態変更通知、非同期イベント処理など
class EventSignal {
private listeners: (() => void)[] = [];
private signaled = false;
signal(): void {
this.signaled = true;
this.listeners.forEach(listener => listener());
this.listeners = [];
}
async wait(): Promise<void> {
if (this.signaled) return;
return new Promise((resolve) => {
this.listeners.push(resolve);
});
}
reset(): void {
this.signaled = false;
}
}
// 使用例:データ更新の通知
const dataUpdatedSignal = new EventSignal();
async function updateData(newData: any) {
await saveData(newData);
dataUpdatedSignal.signal(); // 更新完了を通知
}
async function waitForDataUpdate() {
await dataUpdatedSignal.wait();
console.log('データが更新されました');
}
Channel・Queue:「注文伝票で厨房とやり取り」
一言: 処理間でデータを順序よく受け渡しする仕組み
レストラン例: ホールスタッフが直接厨房に叫ぶのではなく、伝票を通じて情報をやり取り。口頭だと「注文3つ聞こえなかった」が起きます
いつ使う: Web Worker通信、非同期パイプラインなど
class Channel<T> {
private queue: T[] = [];
private waitingReceivers: ((value: T) => void)[] = [];
async send(value: T): Promise<void> {
if (this.waitingReceivers.length > 0) {
const receiver = this.waitingReceivers.shift()!;
receiver(value);
} else {
this.queue.push(value);
}
}
async receive(): Promise<T> {
if (this.queue.length > 0) {
return this.queue.shift()!;
}
return new Promise((resolve) => {
this.waitingReceivers.push(resolve);
});
}
}
// 使用例:ワーカーとの通信
const taskChannel = new Channel<string>();
// Producer(タスク生成者)
async function producer() {
for (let i = 0; i < 10; i++) {
await taskChannel.send(`Task ${i}`);
console.log(`タスク${i}を送信`);
}
}
// Consumer(タスク処理者)
async function consumer() {
while (true) {
const task = await taskChannel.receive();
console.log(`処理中: ${task}`);
await processTask(task);
}
}
4. 安全制御:トラブル時の対応策
Circuit Breaker:「厨房火災時は営業停止」
一言: 連続失敗時に処理を一時停止し、システムを保護する
レストラン例: 厨房で火災が3回続いたら営業停止。「今度こそ大丈夫」と毎回再開するのは消防署が来ます
いつ使う: API障害時の保護。壊れたAPIを叩き続けても、相手も自分も疲弊するだけです
class CircuitBreaker {
private failureCount = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private threshold: number,
private timeout: number
) {}
async call<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
}
}
// 使用例:API呼び出しの保護
const apiBreaker = new CircuitBreaker(5, 60000); // 5回失敗で1分間停止
async function callExternalAPI() {
return apiBreaker.call(async () => {
return await fetch('/external-api');
});
}
Retry + Backoff:「失敗したら間隔を空けて再試行」
一言: 失敗時に指数的に間隔を広げながら再試行する
レストラン例: 食材配送が来ない。1分後に電話、2分後にもう一度、4分後にもう一度。毎秒電話したら着信拒否されます
いつ使う: ネットワーク通信全般。即座にリトライすると障害中のサーバーをさらに追い込みます
class RetryWithBackoff {
constructor(
private maxRetries: number,
private baseDelayMs: number,
private maxDelayMs: number = 30000
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === this.maxRetries) {
break; // 最後の試行なのでリトライしない
}
// 指数バックオフで待機
const delay = Math.min(
this.baseDelayMs * Math.pow(2, attempt),
this.maxDelayMs
);
console.log(`試行 ${attempt + 1} 失敗、${delay}ms後に再試行`);
await this.sleep(delay);
}
}
throw lastError!;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用例:API呼び出しのリトライ
const retry = new RetryWithBackoff(3, 1000);
async function reliableAPICall() {
return retry.execute(async () => {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
}
Timeout:「注文は30分でタイムアウト」
一言: 処理時間に上限を設け、超過時はエラーとする
レストラン例: 注文から30分経っても料理が出なければキャンセル
いつ使う: API呼び出し、長時間処理の制限など
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
})
]);
}
// 使用例:API呼び出しのタイムアウト
async function callAPIWithTimeout() {
try {
const response = await withTimeout(
fetch('/api/slow-endpoint'),
5000 // 5秒でタイムアウト
);
return response.json();
} catch (error) {
if (error.message.includes('timed out')) {
console.log('API呼び出しがタイムアウトしました');
}
throw error;
}
}
// AbortController版(推奨)
async function callAPIWithAbort() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/data', {
signal: controller.signal
});
clearTimeout(timeoutId);
return response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
Bulkhead:「火災は厨房だけ、客席は安全」
一言: システムを分離し、一部の障害が全体に波及しないようにする
レストラン例: 厨房で火災が起きても、客席は防火壁で安全。船の「隔壁(Bulkhead)」が語源です。1区画が浸水しても沈まない
いつ使う: リソース分離。外部API障害で自分のサービス全体が道連れになるのを防ぎます
class BulkheadPool<T> {
private pools: Map<string, T[]> = new Map();
private maxSizes: Map<string, number> = new Map();
createPool(name: string, maxSize: number): void {
this.pools.set(name, []);
this.maxSizes.set(name, maxSize);
}
async acquire(poolName: string, factory: () => T): Promise<T> {
const pool = this.pools.get(poolName) || [];
const maxSize = this.maxSizes.get(poolName) || 10;
if (pool.length > 0) {
return pool.pop()!;
}
if (pool.length >= maxSize) {
throw new Error(`Pool ${poolName} is exhausted`);
}
return factory();
}
release(poolName: string, resource: T): void {
const pool = this.pools.get(poolName);
if (pool) {
pool.push(resource);
}
}
}
// 使用例:API接続の分離
const connectionPool = new BulkheadPool<any>();
// 重要なAPIとそうでないAPIを分離
connectionPool.createPool('critical', 5);
connectionPool.createPool('normal', 3);
async function callCriticalAPI() {
const connection = await connectionPool.acquire('critical', () => {
return createConnection('critical-api.example.com');
});
try {
return await useConnection(connection);
} finally {
connectionPool.release('critical', connection);
}
}
async function callNormalAPI() {
const connection = await connectionPool.acquire('normal', () => {
return createConnection('api.example.com');
});
try {
return await useConnection(connection);
} finally {
connectionPool.release('normal', connection);
}
}
フロントエンド実務での使用頻度マトリクス
実際のフロントエンド開発での使用頻度を整理しました。
| 頻度 | パターン | 主な用途 |
|---|---|---|
| 超高頻度 | デバウンス | 検索入力、リサイズ処理 |
| スロットル | スクロール、ボタン連打防止 | |
| Timeout | API呼び出し、長時間処理 | |
| 高頻度 | Mutex | ファイル操作、状態更新 |
| Retry + Backoff | ネットワーク通信 | |
| Channel・Queue | Worker通信、非同期処理 | |
| 中頻度 | Rate Limit | API制限、スパム防止 |
| Circuit Breaker | 外部サービス連携 | |
| Event・Signal | コンポーネント間通信 | |
| 低頻度 | Semaphore | リソース制限 |
| Barrier | バッチ処理同期 | |
| Latch | 初期化処理 | |
| RWLock | キャッシュ管理 | |
| Bulkhead | システム分離 | |
| Spinlock | 超短時間排他(稀) |
まとめ:全部覚えなくていい。この3つだけ押さえろ
17パターンすべてを覚える必要はありません。フロントエンド開発で必須なのは、次の3つです。
1. デバウンス
const debouncedSearch = debounce(searchAPI, 300);
検索入力やリサイズ処理で必須。 これがないとAPIが連続実行されます。
2. スロットル
const throttledScroll = throttle(updateUI, 100);
スクロールやボタン連打防止で必須。 パフォーマンスを守ります。
3. Timeout
const response = await withTimeout(fetch('/api'), 5000);
API呼び出しで必須。 無限待機を防ぎます。
この3つがあれば、コードレビューで「並行処理の制御がない」と指摘されることはなくなります。
他のパターンは、必要になったときに調べれば十分です。まずはこの3つを確実にマスターして、安全で快適なフロントエンドアプリケーションを作りましょう。
17パターン、全部覚える必要はありません。デバウンス・スロットル・Timeout。この3つが入っているだけで、コードレビューの赤い通知は激減します。
残りの14パターンは、障害が起きたときに「ああ、あのレストランのやつか」と思い出せれば十分です。
