閉じる

モダンC++のスレッド入門:std::jthreadの使い方と同期処理の基本を解説

C++におけるマルチスレッドプログラミングは、C++11での標準ライブラリ導入以来、大きな進化を遂げてきました。

現代のソフトウェア開発において、マルチコアCPUの性能を最大限に引き出す並行処理の実装は不可欠なスキルとなっています。

2026年現在のモダンなC++開発において、スレッド管理のスタンダードとなっているのがC++20で導入されたstd::jthreadです。

本記事では、従来のstd::threadとの違いを明確にしながら、std::jthreadの基本的な使い方や同期処理のベストプラクティスを解説します。

これからC++で並行処理を学ぶ方はもちろん、古いスタイルのスレッド操作から脱却したい方にとっても役立つ内容となっています。

モダンC++におけるスレッド管理の変遷

C++11以前、スレッドを扱うにはプラットフォーム固有のAPIであるPOSIX Threads(pthreads)やWindows APIを直接使用する必要がありました。

C++11によって標準ライブラリにstd::threadが導入されたことで、プラットフォームに依存しない共通のインターフェースで並行処理が記述可能になりました。

しかし、std::threadには、プログラマが明示的にjoin()(終了待機)またはdetach()(切り離し)を呼び出さなければならないという制約がありました。

もしこれらの関数を呼び出さずにstd::threadオブジェクトが破棄されると、実行プログラムは強制終了(std::terminate)してしまいます。

この挙動はリソース管理におけるRAII(Resource Acquisition Is Initialization)の原則に反しており、多くのバグの原因となってきました。

このような背景から、C++20ではより安全で高機能なstd::jthreadが導入されました。

2026年現在の開発現場では、特別な理由がない限り、std::threadではなくstd::jthreadを選択することが推奨されています。

std::jthreadがもたらすメリット

std::jthreadの「j」は「Joining」を意味しており、その名の通り自動的にジョインを行う機能を備えています。

オブジェクトがスコープを抜ける際にデストラクタが実行され、スレッドの終了を自動で待機するため、プログラムの異常終了を防ぐことができます。

また、std::jthreadは「協力的な中断(Cooperative Interruption)」をサポートしており、外部からスレッドの停止を安全に要求する仕組みが組み込まれています。

これにより、ループ処理を行っているスレッドに対して、フラグ管理を自作することなく停止信号を送ることが可能になりました。

std::jthreadの基本的な使い方

まずは、最もシンプルなstd::jthreadの使用例を見ていきましょう。

以下のコードは、バックグラウンドでメッセージを表示するスレッドを起動する例です。

C++
#include <iostream>
#include <thread>
#include <chrono>

void worker_function() {
    // 1秒間スリープして処理をシミュレート
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Worker thread: 処理が完了しました。" << std::endl;
}

int main() {
    std::cout << "Main thread: スレッドを開始します。" << std::endl;

    // jthreadを作成。スコープを抜けるときに自動でjoinされる
    {
        std::jthread jt(worker_function);
    } 

    std::cout << "Main thread: すべての処理が終了しました。" << std::endl;
    return 0;
}
実行結果
Main thread: スレッドを開始します。
Worker thread: 処理が完了しました。
Main thread: すべての処理が終了しました。

このコードでは、std::jthreadのオブジェクトjtが中括弧のスコープを抜けるタイミングでデストラクタが呼ばれます。

その際、自動的にスレッドの終了を待つため、従来のstd::threadのように明示的なjt.join()を記述する必要がありません。

スレッドへの引数渡し

スレッドで実行する関数に対して引数を渡す方法は、関数の引数リストをstd::jthreadのコンストラクタに追加するだけです。

引数はコピーされてスレッドに渡されますが、参照渡しを行いたい場合はstd::refを使用する必要があります。

C++
#include <iostream>
#include <thread>
#include <string>

void print_message(const std::string& msg, int count) {
    for (int i = 0; i < count; ++i) {
        std::cout << msg << " (ID: " << i << ")" << std::endl;
    }
}

int main() {
    std::string text = "Hello from jthread";
    // 引数として文字列と回数を渡す
    std::jthread jt(print_message, text, 3);
    
    return 0;
}
実行結果
Hello from jthread (ID: 0)
Hello from jthread (ID: 1)
Hello from jthread (ID: 2)

協力的な中断:stop_tokenの活用

モダンC++のスレッド操作において最も重要な概念の一つが、協力的な中断です。

これは、実行中のスレッドに対して「処理を中断してほしい」という要求を送り、スレッド側が適切なタイミングで自ら終了する仕組みです。

std::jthreadは、実行する関数の第一引数にstd::stop_tokenを受け取ることができます。

stop_tokenを使用したループの終了

以下の例では、メインスレッドから停止要求を送り、ワーカースレッドがそれを検知してループを抜ける様子を示します。

C++
#include <iostream>
#include <thread>
#include <chrono>

void continuous_worker(std::stop_token stoken) {
    std::cout << "Worker: 監視を開始します。" << std::endl;

    // 外部から停止要求(stop_requested)があるまでループを継続
    while (!stoken.stop_requested()) {
        std::cout << "Worker: 実行中..." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    std::cout << "Worker: 停止要求を検知し、安全に終了します。" << std::endl;
}

int main() {
    std::jthread jt(continuous_worker);

    // 2秒間メインスレッドを待機
    std::this_thread::sleep_for(std::chrono::seconds(2));

    std::cout << "Main: スレッドの停止を要求します。" << std::endl;
    // request_stop()を呼び出して停止信号を送る
    jt.request_stop();

    return 0;
}
実行結果
Worker: 監視を開始します。
Worker: 実行中...
Worker: 実行中...
Worker: 実行中...
Worker: 実行中...
Main: スレッドの停止を要求します。
Worker: 停止要求を検知し、安全に終了します。

このように、std::stop_tokenを使用することで、スレッド間で共有するフラグ変数を自前で用意する必要がなくなりました。

また、std::jthreadのデストラクタが呼ばれる際にも自動的にrequest_stop()が実行されるため、非常に安全な設計となっています。

スレッド間の同期処理とデータの共有

複数のスレッドが同じメモリ領域に同時にアクセスし、少なくとも一方が書き込みを行う場合、データ競合(Data Race)が発生します。

データ競合は未定義動作を引き起こし、デバッグが極めて困難なバグの原因となります。

この問題を回避するためには、適切な同期メカニズムを使用して、排他的なアクセスを保証しなければなりません。

std::mutexによる排他制御

最も基本的な同期手段は、std::mutex(ミューテックス)を使用したロックです。

ミューテックスをロックしたスレッドだけが、その保護下にあるデータにアクセスできる権利を得ます。

モダンな実装では、直接lock()unlock()を呼ぶのではなく、RAIIをサポートするラッパーを使用することが鉄則です。

std::lock_guardとstd::unique_lock

std::lock_guardは、コンストラクタでロックを取得し、デストラクタで解放する最もシンプルなラッパーです。

より高度な操作(手動でのロック解除や時間制限付きロックなど)が必要な場合は、std::unique_lockを使用します。

C++
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

int shared_counter = 0;
std::mutex counter_mutex;

void increment(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        // ミューテックスをロック。スコープ終了時に自動解除
        std::lock_guard<std::mutex> lock(counter_mutex);
        shared_counter++;
    }
}

int main() {
    const int num_threads = 5;
    const int iterations = 10000;
    std::vector<std::jthread> threads;

    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(increment, iterations);
    }

    // すべてのjthreadが終了するのを待機
    threads.clear(); 

    std::cout << "最終カウント結果: " << shared_counter << std::endl;
    return 0;
}

上記のコードでは、std::lock_guardによってshared_counterへのアクセスが一度に一つのスレッドのみに制限されています。

もしこのロックがない場合、複数のスレッドが同時に加算処理を行うため、最終的な値は50000にならず、予期しない小さな値になってしまいます。

std::scoped_lockによるデッドロックの回避

複数のミューテックスを同時にロックする必要がある場合、ロックの順序を間違えるとデッドロックが発生するリスクがあります。

C++17で導入されたstd::scoped_lockは、複数のミューテックスを一度に、かつ安全な順序でロックするための非常に便利なクラスです。

C++
#include <mutex>

std::mutex mutexA;
std::mutex mutexB;

void process_data() {
    // 複数のミューテックスを安全に一括ロック
    std::scoped_lock lock(mutexA, mutexB);
    // ここで両方のリソースを安全に操作
}

このように記述することで、ロックの順序が内部で最適化され、循環待機によるデッドロックを防ぐことができます。

効率的な同期:std::atomicの活用

数値のカウントアップやフラグのチェックなどの単純な処理に、重いミューテックスを使用するのは効率的ではありません。

このようなケースでは、CPUレベルでアトミック(不可分)な操作を保証するstd::atomicを使用するのが最適です。

std::atomicを使用すると、明示的なロック記述なしにスレッドセーフな操作が可能になり、パフォーマンスが大幅に向上します。

C++
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

// アトミックな整数型
std::atomic<int> atomic_counter{0};

void atomic_increment(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        // ロックなしで安全に加算
        atomic_counter++;
    }
}

int main() {
    std::vector<std::jthread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(atomic_increment, 10000);
    }
    
    threads.clear();
    std::cout << "アトミックカウント結果: " << atomic_counter.load() << std::endl;
    return 0;
}

std::atomicはミューテックスのような「待機」が発生しにくいため、高頻度で更新されるデータの管理に適しています。

ただし、複数の変数にわたる整合性を保つ必要がある場合は、やはりミューテックスによる一括管理が必要です。

条件変数とイベント待機

あるスレッドが「特定の条件が満たされるまで待機する」という処理を実装する場合、ビジーウェイト(ループによる監視)はCPUリソースを浪費します。

このような場面では、std::condition_variableを使用して、効率的に待機と通知を行うのが一般的です。

C++20以降では、std::jthreadと組み合わせて使いやすいstd::condition_variable_anyなども利用可能です。

プロデューサー・コンシューマーパターンの例

データを生成する側(プロデューサー)と消費する側(コンシューマー)のやり取りを実装してみましょう。

C++
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> data_queue;
std::mutex queue_mutex;
std::condition_variable cv;
bool finished = false;

void producer() {
    for (int i = 1; i <= 5; ++i) {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            data_queue.push(i);
            std::cout << "Produced: " << i << std::endl;
        }
        cv.notify_one(); // 待機中のスレッドに通知
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    
    {
        std::lock_guard<std::mutex> lock(queue_mutex);
        finished = true;
    }
    cv.notify_all();
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(queue_mutex);
        // 条件が満たされるまで待機(データの存在または終了フラグ)
        cv.wait(lock, [] { return !data_queue.empty() || finished; });

        while (!data_queue.empty()) {
            int val = data_queue.front();
            data_queue.pop();
            std::cout << "Consumed: " << val << std::endl;
        }

        if (finished) break;
    }
}

int main() {
    std::jthread p_thread(producer);
    std::jthread c_thread(consumer);
    return 0;
}

この例では、コンシューマースレッドがcv.waitで効率的にスリープし、新しいデータが追加された時だけ活動を再開します。

これにより、無駄なCPU消費を抑えつつ、レスポンスの良いシステムを構築できます。

C++20/23/26に向けた発展的な並行処理

2026年現在、C++の並行処理は単なるスレッド管理を超え、より抽象度の高いモデルへと移行しつつあります。

特にSenders/Receiversと呼ばれるモデルは、非同期処理の連鎖を効率的に記述するための新しい標準として注目されています。

しかし、こうした高度な抽象化の下支えとなっているのは、依然として本記事で解説したstd::jthreadや同期プリミティブの概念です。

バリアとラッチ(C++20)

複数のスレッドが特定のポイントで合流するための仕組みとして、std::latchstd::barrierが導入されました。

これらを使用すると、「10個のスレッドすべてが準備完了になるまで待機する」といった同期が非常に簡潔に記述できます。

機能概要主な用途
std::latch一度きりのカウントダウン同期初期化完了の待機など
std::barrier繰り返し利用可能な同期ポイント並列反復計算のステップ同期
std::semaphoreリソースの利用可能数を管理接続数制限など

マルチスレッド開発における注意点

強力な並行処理ですが、不適切な使用はパフォーマンスの低下や予測不能なエラーを招きます。

まず、スレッドの生成には一定のオーバーヘッドがあるため、極端に短い処理のために頻繁にスレッドを立てるのは避けるべきです。

また、「偽共有(False Sharing)」と呼ばれる、無関係な変数が同じキャッシュラインに載ることで性能が低下する現象にも注意が必要です。

共有データは最小限にし、可能な限り各スレッドが独立したデータに対して処理を行う「スレッドローカルストレージ」の活用を検討してください。

まとめ

モダンC++におけるマルチスレッドプログラミングの基本を解説してきました。

2026年現在のC++開発では、安全で自動的なリソース管理を提供するstd::jthreadの採用が第一の選択肢となります。

それに加え、std::stop_tokenを活用した協力的な中断、std::scoped_lockによるデッドロックの防止など、標準ライブラリが提供する機能を正しく組み合わせることが重要です。

並行処理は複雑なトピックですが、RAIIに基づいた設計を心がけることで、堅牢で高性能なアプリケーションを構築できるようになります。

本記事で紹介したコードや概念をベースに、より高度な並行アルゴリズムや非同期プログラミングの世界へ挑戦してみてください。

URLをコピーしました!