0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIコードレビューのハーネスを設計するために整理した並行処理の制御パターン全17種

0
Last updated at Posted at 2026-03-29

AIコードレビューの観点を整理したら、意外と知らない制御があった

Claude CodeやCopilotにコードレビューを任せるためのハーネスを構築していたときのことです。レビュー観点のチェックリストに「並行処理の制御」を追加しようとして、手が止まりました。

Mutex、デバウンス、Circuit Breakerあたりは書ける。でも「全部で何種類あるのか」と聞かれると、答えられない。

AIレビューに観点を教えるには、まず自分が全体像を把握していないと話にならない。そこで整理してみたら、17パターンもありました。しかも実務で意識していなかったものが結構ある。

この記事では、その17パターンを「レストラン運営」に例えて整理します。レストランも並行処理も、限られたリソースで複数の要求を同時にさばく必要がある点で同じです。

全体マップ:4つのカテゴリで整理する制御の世界

並行処理制御パターン全17種

並行処理の制御パターンは、レストラン運営に例えると理解しやすくなります。

レストランも並行処理も、限られたリソース(厨房・スタッフ・席)を効率的に使って、複数の要求(お客様の注文)を同時に処理する必要があります。

4つのカテゴリに分類できます:

  1. 排他制御(誰が使えるか) - 厨房の使用権管理
  2. タイミング制御(いつ実行するか) - 注文タイミングの調整
  3. 協調制御(順番・合図) - スタッフ間の連携
  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パターンは、障害が起きたときに「ああ、あのレストランのやつか」と思い出せれば十分です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?