マルチスレッドプログラミングで頭を悩ませるのが、複数のスレッドからアクセスされる変数の管理です。すべてのスレッドで共有されるstatic
変数は、データ競合を防ぐためにロック(Mutex)が必須となり、コードを複雑にします。
しかし、「スレッド間では共有したくないが、スレッド内では関数の呼び出しを超えて値を保持したい」というケースも多くあります。この記事では、そのような要求をエレガントに解決する**thread_local
**指定子について、その仕組みと具体的な使い方を解説します。
変数の生存期間をコードで比較
thread_local
の挙動を理解するために、3種類の変数(static
、thread_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
が使えないか検討してみると良いでしょう。