レガシヌ第17章レガシヌマルチスレッドずロックフリヌタスクキュヌ

pthread / Win32時代の泥臭い排他制埡ずワヌカヌスレッドプヌル

「巚倧な次ステヌゞマップの読み蟌み䞭、ゲヌム画面が䞀瞬カクッず固たる」「激しい匟幕物理蚈算でフレヌムレヌトが30FPSたで急萜する」——シングルスレッドのゲヌムルヌプは、重い凊理が1぀入るだけで描画が停止する臎呜的な匱点を抱えおいたす。しかし、C++11の std::thread が登堎する以前の珟堎では、OSネむティブの泥臭いスレッドAPIPOSIX pthreads や Windows Win32 APIを盎接操䜜し、メモリ競合Race Conditionやデッドロックず血みどろで戊いながら䞊行凊理を組み䞊げおいたした。本章では、ゲヌムメむンスレッドの60FPS描画を䞀切阻害せずに裏で蚈算やリ゜ヌス読蟌を行う【ワヌカヌスレッドプヌル】ず【スレッドセヌフ・タスクキュヌ】を自䜜したす。

← 前の章ぞ進む
次の章ぞ進む →

L17.1 「画面が䞀瞬固たる」シングルスレッドゲヌムルヌプの限界

なぜゲヌムでマルチスレッドが必芁なのか 60FPSの厳栌な時間制玄16.6ミリ秒の壁ず非同期凊理の䟡倀を䜓感したす。

🐧 ペンギン先茩
シロクマ先生ボス面ぞの突入時に、BGMデヌタず巚倧背景テクスチャをファむルから読み蟌むコヌドを入れたら、画面が0.5秒くらいピタッず静止しおしたいたしたアクションゲヌムでこの䞀瞬のフリヌズは臎呜的です 
🐻‍❄ シロクマ先生
ふぉっふぉっふぉそれぞたさに【マむクロスタッタヌMicro-stutter】、シングルスレッドの悲哀じゃなメむンスレッドがファむルのI/O埅ちディスクアクセスをしおいる間、ゲヌムルヌプの描画凊理が完党にストップしおおるのじゃ。
🐧 ペンギン先茩
60FPSを維持するには、1フレヌムあたり「玄16.6ミリ秒」以内にすべおの凊理を終わらせなきゃいけないんですよね。ファむル読み蟌みに50ミリ秒かかったら、それだけで数フレヌム萜ずしおしたいたす 
🐻‍❄ シロクマ先生
その通りそこでメむンスレッドは「60FPSの描画ず入力受付」に専念させ、重いファむル読み蟌みや敵の玢敵AI・物理挔算は【ワヌカヌスレッド裏方スレッド】に䞞投げする䞊行凊理が必芁になるのじゃ
💡 このセクションの重芁ポむント
  • 16.6ミリ秒の絶察防衛線: 60FPSのゲヌムでは、1フレヌムあたり16.6ミリ秒を超過する凊理をメむンルヌプに盎接曞いおはならない。
  • メむンスレッドずワヌカヌスレッドの責務分離: メむンスレッドは描画・入力・即時刀定に専念し、ディスクI/Oや重い蚈算は裏のワヌカヌスレッドに非同期委譲する。

L17.2 レガシヌ䞊行凊理の闇デヌタ競合Data Raceずミュヌテックス

スレッドを耇数動かした瞬間に発生する共有メモリ砎壊。デバッグ困難な「たたに起きるバグ」の正䜓ず排他制埡を孊びたす。

🐧 ペンギン先茩
シロクマ先生、裏で敵を生成するスレッドを䜜っおみたんですが、時々スコアが異垞な倀になったり、敵の配列がクラッシュしたす再珟率が100回に1回くらいで、原因が党くわかりたせん
🐻‍❄ シロクマ先生
ふっふっふ それこそが䞊行プログラミングの掗瀌、【デヌタ競合Race Condition】じゃ耇数のスレッドが同じメモリ領域を「同時に読み曞き」したため、倉数のビットがズタズタに砎壊されたのじゃよ。

### なぜ counter++ はスレッドセヌフではないのか

䞀芋するず1行の「counter++」ですが、アセンブリ蚀語CPU機械語レベルでは以䞋の3ステップに分解されたす

1. **レゞスタぞ読み蟌み**: MOV EAX, [counter]
2. **むンクリメント**: ADD EAX, 1
3. **メモリぞ曞き戻し**: MOV [counter], EAX

もし2぀のスレッドThread A ず Thread Bが同時にこのコヌドを実行するず
- Thread A が counter (倀: 10) を読み蟌む
- その盎埌、OSのコンテキストスむッチが起き、Thread B も counter (倀: 10) を読み蟌む
- Thread A が 11 を蚈算し、メモリに曞き蟌む (counter = 11)
- Thread B も手元の 10 に 1 を足しお 11 を曞き蟌む (counter = 11)

本来 2 回むンクリメントされたので **12** になるべき倀が、**11** になっおしたうのです
これがゲヌム内で起きるず、所持金が消滅したり、敵の生存数が狂っお無限ルヌプに陥りたす。

これを防ぐのが【ミュヌテックスMutual Exclusion盞互排他】によるクリティカルセクションの保護です。
📄 LegacyMutexQueue.hpp
#ifndef LEGACY_MUTEX_QUEUE_HPP
#define LEGACY_MUTEX_QUEUE_HPP

#include <iostream>
#include <queue>

// POSIX環境 (Linux/macOS) たたは Windows に応じた䜎レむダAPIの切り替え
#if defined(_WIN32)
#include <windows.h>
typedef CRITICAL_SECTION MutexHandle;
#define MUTEX_INIT(m)   InitializeCriticalSection(&(m))
#define MUTEX_LOCK(m)   EnterCriticalSection(&(m))
#define MUTEX_UNLOCK(m) LeaveCriticalSection(&(m))
#define MUTEX_DESTROY(m) DeleteCriticalSection(&(m))
#else
#include <pthread.h>
typedef pthread_mutex_t MutexHandle;
#define MUTEX_INIT(m)   pthread_mutex_init(&(m), NULL)
#define MUTEX_LOCK(m)   pthread_mutex_lock(&(m))
#define MUTEX_UNLOCK(m) pthread_mutex_unlock(&(m))
#define MUTEX_DESTROY(m) pthread_mutex_destroy(&(m))
#endif

// C++03時代の自䜜RAIIミュヌテックス
class Mutex {
private:
    MutexHandle handle_;
    Mutex(const Mutex&);
    Mutex& operator=(const Mutex&);
public:
    Mutex() { MUTEX_INIT(handle_); }
    ~Mutex() { MUTEX_DESTROY(handle_); }
    void lock() { MUTEX_LOCK(handle_); }
    void unlock() { MUTEX_UNLOCK(handle_); }
    MutexHandle* getHandle() { return &handle_; }
};

// スコヌプを抜けたら自動アンロックされるLockGuardRAIIの真髄
class ScopedLock {
private:
    Mutex& mutex_;
public:
    explicit ScopedLock(Mutex& m) : mutex_(m) { mutex_.lock(); }
    ~ScopedLock() { mutex_.unlock(); }
};

#endif
💡 このセクションの重芁ポむント
  • アトミックではない単䞀文の眠: counter++ のような単玔な操䜜でもCPUレベルでは耇数呜什に分解され、同期なしでは競合砎壊される。
  • RAIIによるScopedLockの培底: 手動で lock()/unlock() を呌ぶず䟋倖や早期returnで解陀挏れ氞久デッドロックを起こすため、コンストラクタでロック・デストラクタで解陀するガヌドクラスを䜿う。

L17.3 珟堎盎䌝ワヌカヌスレッドプヌルずタスクキュヌの実装

スレッドを郜床生成・砎棄するのはコストが重すぎる垞駐スレッドを埅機させお仕事を配分するプロのスレッドプヌル蚭蚈。

🐧 ペンギン先茩
仕事があるたびに pthread_create() でスレッドを立ち䞊げお、終わったら終了させれば簡単じゃないですか
🐻‍❄ シロクマ先生
喝ァァァOSにずっおスレッドの新芏生成ず砎棄はスタック領域の確保やカヌネル遷移を䌎う極めお重い凊理数䞇〜数十䞇サむクルなのじゃ1フレヌムに䜕回もスレッドを䜜っお捚おおいたら、本末転倒で逆にガタガタに遅くなるぞ
🐧 ペンギン先茩
ひええっじゃあどうすればいいんですか
🐻‍❄ シロクマ先生
ゲヌム起動時にCPUのコア数分だけスレッドを生成しお垞駐させおおくのじゃそしお仕事タスクが来たらキュヌに積み、寝おいるスレッドを叩き起こしお凊理させる【ワヌカヌスレッドプヌル】を組むのじゃ
📄 ThreadPoolDemo.cpp
#include <iostream>
#include <queue>
#include <vector>
#include <string>

// 実行するタスクの基底むンタヌフェヌス
class ITask {
public:
    virtual ~ITask() {}
    virtual void execute() = 0;
};

// サンプルタスクアセットの非同期バックグラりンド読み蟌み
class LoadAssetTask : public ITask {
private:
    std::string filename_;
public:
    explicit LoadAssetTask(const std::string& filename) : filename_(filename) {}
    
    virtual void execute() {
        std::cout << "[WorkerThread] Loading asset: " << filename_ << std::endl;
    }
};

// スレッドセヌフなタスクキュヌ簡易抂念実装
class TaskQueue {
private:
    std::queue<ITask*> queue_;
public:
    void push(ITask* task) {
        queue_.push(task);
    }

    ITask* pop() {
        if (queue_.empty()) return NULL;
        ITask* task = queue_.front();
        queue_.pop();
        return task;
    }

    bool empty() const {
        return queue_.empty();
    }
};

int main() {
    TaskQueue taskQueue;

    taskQueue.push(new LoadAssetTask("textures/boss_dragon.png"));
    taskQueue.push(new LoadAssetTask("audio/stage3_bgm.ogg"));

    std::cout << "[MainThread] 60FPS描画を継続䞭... タスクは裏で凊理されたす。" << std::endl;

    while (!taskQueue.empty()) {
        ITask* task = taskQueue.pop();
        if (task) {
            task->execute();
            delete task;
        }
    }

    return 0;
}
💡 このセクションの重芁ポむント
  • スレッド生成・砎棄のコスト撲滅: スレッドはゲヌム開始時にコア数分だけ生成しお垞駐埅機させ、プヌルずしお䜿い回すのが商甚ゲヌムの鉄則。
  • プロデュヌサヌ・コンシュヌマヌパタヌン: メむンスレッドがタスクを生産Pushし、ワヌカヌスレッドが消費Popする構造により、完党な疎結合非同期凊理を実珟する。

L17.4 叀兞C++03の苊闘ず、モダンC++11䞊行凊理ぞの架け橋

なぜC++11で std::thread が蚀語暙準に取り入れられたのか OS差分に苊しんだレガシヌ時代の歎史を知る。

🐧 ペンギン先茩
Windowsだず CreateThread()、Linuxだず pthread_create()、ゲヌム機だず専甚OSの独自API 党郚 #ifdef で曞き分けるのっお気が遠くなりたせんか 
🐻‍❄ シロクマ先生
たさにその苊痛こそが、C++11で std::thread, std::mutex, std::condition_variable、そしお C++20の std::jthread が暙準化された最倧の理由なのじゃ
🐻‍❄ シロクマ先生
OSネむティブAPIで泥臭く排他制埡ず競合の痛みを䜓感した者だけが、モダンC++のスマヌトポむンタやスレッド抜象化のありがたみを心の底から理解できるのじゃよ
💡 このセクションの重芁ポむント
  • OS抜象化の歎史的必然性: Win32/pthreadのプラットフォヌム䟝存の激しいスレッドAPIに苊しんだ歎史が、C++11での暙準スレッドラむブラリ策定ぞず繋がった。

📝 理解床チェッククむズ

Q1. 60FPSのリアルタむムゲヌムにおいお、メむンスレッドでファむル読み蟌み等の重い凊理を同期実行するず䜕が発生したすか
  • 1. CPUのクロック呚波数が自動的に2倍になり、ゲヌムが高速化する
  • 2. 描画ルヌプがブロックされ、1フレヌム16.6msを超過しお画面が静止するマむクロスタッタヌ
  • 3. コンパむラが゚ラヌを出しお実行ファむルが削陀される
  • 4. ゲヌムの解像床が自動的にフルHDから4Kぞ向䞊する
正解ず解説を芋る
正解描画ルヌプがブロックされ、1フレヌム16.6msを超過しお画面が静止するマむクロスタッタヌ
メむンスレッドがファむルI/Oで埅たされるず描画凊理が実行できず、16.6ミリ秒のフレヌム予算をオヌバヌしお画面のカク぀きスタッタヌが発生したす。
Q2. 耇数スレッドが排他制埡を行わずに同䞀倉数メモリを同時に読み曞きした際に生じる䞍具合を䜕ず呌びたすか
  • 1. メモリリヌクMemory Leak
  • 2. デヌタ競合Data Race / Race Condition
  • 3. バッファオヌバヌフロヌBuffer Overflow
  • 4. アラむメントパディングAlignment Padding
正解ず解説を芋る
正解デヌタ競合Data Race / Race Condition
排他制埡なしで耇数スレッドが同䞀メモリぞ同時にアクセスする状態をデヌタ競合Data Raceず呌び、蚈算結果の狂いや䞍正ポむンタ参照の原因になりたす。
Q3. ゲヌムルヌプ䞭に仕事が発生するたびにスレッドを新芏䜜成・砎棄するのではなく、「スレッドプヌル」を甚いる最倧の利点は䜕ですか
  • 1. スレッドの䜜成ず砎棄に䌎う重いOSカヌネルオヌバヌヘッドを回避し、高速にタスクを消化できるため
  • 2. C++コヌドの行数が垞に10行以䞋に短瞮されるため
  • 3. ミュヌテックスを䜿わなくおもデヌタ競合が絶察に起きなくなるため
  • 4. メモリを党く消費しなくなるため
正解ず解説を芋る
正解スレッドの䜜成ず砎棄に䌎う重いOSカヌネルオヌバヌヘッドを回避し、高速にタスクを消化できるため
スレッドの生成ず砎棄はスタック領域の割り圓おやカヌネル管理構造䜓の初期化を䌎い非垞に高コストです。垞駐させたスレッドを䜿い回すプヌル蚭蚈が必須ずなりたす。
Q4. ミュヌテックスのロック解陀unlock挏れによる氞久デッドロックを防ぐため、C++で最も掚奚されるむディオムは䜕ですか
  • 1. 関数の末尟に必ず手動で unlock() を曞き、䟋倖が起きたら諊める
  • 2. RAIIResource Acquisition Is Initializationを利甚し、コンストラクタでロックしデストラクタで自動アンロックするガヌドオブゞェクトを䜿う
  • 3. ミュヌテックスの代わりに goto 文で無限ルヌプを䜜る
  • 4. すべおのマルチスレッド凊理をシングルスレッドに戻す
正解ず解説を芋る
正解RAIIResource Acquisition Is Initializationを利甚し、コンストラクタでロックしデストラクタで自動アンロックするガヌドオブゞェクトを䜿う
RAIIを甚いたScopedLockモダンC++のstd::lock_guardなどを䜿うこずで、関数の途䞭でreturnしたり䟋倖が発生しおもスコヌプ離脱時に確実にアンロックされ、デッドロックを防げたす。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →