ロック不要で安全?thread_localでスレッドごとの状態を管理する方法

マルチスレッドプログラミングで頭を悩ませるのが、複数のスレッドからアクセスされる変数の管理です。すべてのスレッドで共有されるstatic変数は、データ競合を防ぐためにロック(Mutex)が必須となり、コードを複雑にします。

しかし、「スレッド間では共有したくないが、スレッド内では関数の呼び出しを超えて値を保持したい」というケースも多くあります。この記事では、そのような要求をエレガントに解決する**thread_local**指定子について、その仕組みと具体的な使い方を解説します。


目次

変数の生存期間をコードで比較

thread_localの挙動を理解するために、3種類の変数(staticthread_local、通常のローカル)がマルチスread環境でどのように振る舞うかを、以下のコードで見てみましょう。

このプログラムは、2つのスレッドを生成し、各スレッドがループ内でprocess_job関数を複数回呼び出します。

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

// 複数のスレッドから共有されるため、保護が必要
std::mutex shared_mutex;

// 各変数の値をインクリメントして表示する関数
void process_job(int value_to_add) {
    // 全スレッドで共有される単一の変数
    static int shared_counter = 0;
    
    // スレッドごとに独立して存在する変数
    thread_local int thread_specific_counter = 0;
    
    // この関数が呼び出されるたびに生成・破棄される変数
    int local_counter = 0;

    // --- カウンタを更新 ---
    shared_counter += value_to_add;
    thread_specific_counter += value_to_add;
    local_counter += value_to_add;
    
    // 共有変数を扱う際は、複数スレッドからの同時アクセスを防ぐためロックする
    std::scoped_lock lock(shared_mutex);
    std::cout << "Thread ID [" << std::this_thread::get_id() << "]"
              << " shared: " << shared_counter
              << ", thread_local: " << thread_specific_counter
              << ", local: " << local_counter << std::endl;
}

// 各スreadが実行するタスク
void thread_task() {
    for (int i = 0; i < 3; ++i) {
        process_job(10);
    }
}

int main() {
    std::vector<std::thread> threads;
    // 2つのスレッドを生成
    for (int i = 0; i < 2; ++i) {
        threads.push_back(std::thread{thread_task});
    }

    for (std::thread& th : threads) {
        th.join();
    }
    
    return 0;
}

実行結果の例と解説

(実行のタイミングにより、スレッドのIDや表示順は変わります)

Thread ID [140737344988480] shared: 10, thread_local: 10, local: 10
Thread ID [140737353381184] shared: 20, thread_local: 10, local: 10
Thread ID [140737344988480] shared: 30, thread_local: 20, local: 10
Thread ID [140737353381184] shared: 40, thread_local: 20, local: 10
Thread ID [140737344988480] shared: 50, thread_local: 30, local: 10
Thread ID [140737353381184] shared: 60, thread_local: 30, local: 10
  • shared (static変数): すべてのスレッドで完全に共有されています。スレッド1が10を加算した後、スレッド2が加算すると20になり、その値はすべてのスレッドから同じように見えます。最終的に60まで累積しているのが分かります。Mutexによる保護が必須です。
  • local (ローカル変数): process_job関数が呼び出されるたびに0で初期化され、10が加算されて関数が終わると破棄されます。そのため、値は常に10です。
  • thread_local (スレッドローカル変数): これが今回の主役です。各スレッドが自分だけの独立したコピーを持っています。スレッド1のカウンタは 10 -> 20 -> 30 と累積し、スレッド2のカウンタも独立して 10 -> 20 -> 30 と累積します。互いに全く干渉しないため、Mutexによる保護が不要です。

thread_localのライフサイクルと使い道

thread_localで宣言された変数は、スレッドが作られた後、その変数が初めて使われる直前に生成されます。そして、そのスレッドが終了する時に破棄されます。

この特性から、以下のような場面で非常に役立ちます。

  • パフォーマンス向上: スレッドごとに独立したデータバッファやキャッシュを持つことで、Mutexによるロックの競合を避け、性能を向上させることができます。
  • スレッド固有の状態管理: スレッドごとに別々の乱数生成器、エラーコード、あるいはログ設定などを保持するのに最適です。
  • 安全なグローバル変数の実現: 元々はスレッドセーフでなかったグローバル変数も、thread_localを付けることで、各スレッドに自分だけのコピーを持たせ、安全に利用できる場合があります。

まとめ

thread_localは、マルチスレッドプログラミングにおける変数管理の問題をスマートに解決する強力なツールです。staticのように関数の呼び出しを超えて値を保持しつつ、各スレッドで完全に独立した領域を確保します。

共有データのロック(Mutex)によるパフォーマンス低下やコードの複雑化に悩んだときは、そのデータが本当にスレッド間で共有される必要があるのかを考え直し、thread_localが使えないか検討してみると良いでしょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次