マルチスレッドプログラミングにおけるスレッドストレージ期間の重要性


スレッドストレージ期間とは、変数がスレッドごとに独立して生成および破棄される期間を指します。つまり、同じ名前の変数が複数のスレッドで使用されていても、各スレッドは独自の変数インスタンスを持つことになります。これは、スレッド間の競合状態を防ぎ、スレッドセーフなプログラムを実現する上で重要です。

スレッドストレージ期間の利点

  • コードの可読性向上: スレッド固有のデータを明確に区別できるため、コードの可読性が向上します。
  • メモリ使用量の削減: 共有変数が必要ない場合、スレッドストレージ変数を使用することでメモリ使用量を削減できます。
  • スレッドセーフなコードを容易に記述可能: 各スレッドが独自の変数を持つため、競合状態のリスクを低減できます。

C11以降では、thread_localキーワードを使用してスレッドストレージ期間変数を宣言できます。構文は以下の通りです。

thread_local 変数名 型別;

例えば、以下のようにスレッドごとに異なるカウント値を保持する変数を宣言できます。

thread_local int count = 0;
  • スレッドストレージ変数のデストラクタは、スレッドが終了する際に呼び出されます。
  • スレッドストレージ変数の初期値は、スレッドごとに個別に初期化されます。
  • thread_localキーワードは、静的変数とextern変数にのみ使用できます。
記憶域期間説明利点欠点
自動変数関数内でのみ有効シンプルスレッド間で共有できない
静的変数プログラム全体で有効スレッド間で共有可能初期化オーバーヘッドがある
スレッドストレージ期間スレッドごとに有効スレッドセーフ、メモリ効率が良い静的変数と比べて初期化オーバーヘッドが大きい

スレッドストレージ期間は、マルチスレッドプログラミングにおける重要な概念です。スレッドセーフなコードを容易に記述し、メモリ使用量を削減し、コードの可読性を向上させるために役立ちます。



#include <stdio.h>
#include <pthread.h>

void* thread_function(void* arg) {
  // スレッドごとに異なるカウント値を保持する変数を宣言
  thread_local int count = 0;

  // 10回カウントアップ
  for (int i = 0; i < 10; i++) {
    count++;
  }

  // カウント値を出力
  printf("スレッド %ld のカウント値: %d\n", pthread_self(), count);

  return NULL;
}

int main() {
  // 3つのスレッドを作成
  pthread_t threads[3];

  for (int i = 0; i < 3; i++) {
    pthread_create(&threads[i], NULL, thread_function, NULL);
  }

  // スレッドの完了を待つ
  for (int i = 0; i < 3; i++) {
    pthread_join(threads[i], NULL);
  }

  return 0;
}

このコードでは、thread_functionというスレッド関数を作成します。この関数は、thread_localキーワードを使用してスレッドごとに異なるカウント値を保持する変数 count を宣言します。その後、count 変数を 10 回カウントアップし、スレッド ID とともにカウント値を出力します。

main 関数は、3 つのスレッドを作成し、それぞれに thread_function 関数を実行させます。その後、すべてのスレッドが完了するまで待機してから、プログラムを終了します。

このコードを実行すると、以下の出力が得られます。

スレッド 1407198480 のカウント値: 10
スレッド 1407198472 のカウント値: 10
スレッド 1407198464 のカウント値: 10

各スレッドは独自の count 変数を持つため、カウント値は互いに干渉しません。

  • コンパイラとリンカーの設定によっては、pthreadライブラリを別途リンクする必要があります。
  • このコードは、pthreadライブラリを使用しています。pthreadライブラリを使用するには、適切なヘッダーファイル (pthread.h) をインクルードし、必要な関数 (pthread_create(), pthread_join()) を呼び出す必要があります。


関数ローカル変数を使用する

最も単純な代替方法は、関数ローカル変数を使用することです。関数ローカル変数は、関数のスコープ内で有効な変数です。つまり、各スレッドは関数を呼び出すたびに、その関数で宣言された変数の新しいインスタンスを取得します。

void thread_function(void* arg) {
  // スレッドごとに異なるカウント値を保持する変数を宣言
  int count = 0;

  // 10回カウントアップ
  for (int i = 0; i < 10; i++) {
    count++;
  }

  // カウント値を出力
  printf("スレッド %ld のカウント値: %d\n", pthread_self(), count);
}

この方法は、シンプルで理解しやすいという利点があります。しかし、複数のスレッドから同じ関数を呼び出す場合、メモリ使用量が多くなるという欠点があります。

カスタム同期メカニズムを使用する

もう 1 つの代替方法は、カスタム同期メカニズムを使用して、共有変数へのアクセスを制御することです。例えば、ミューテックスやセマフォアなどの同期オブジェクトを使用することができます。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* thread_function(void* arg) {
  // 共有変数にアクセスする前にミューテックスを取得
  pthread_mutex_lock(&mutex);

  // 共有変数を更新
  static int count = 0;
  count++;

  // 共有変数へのアクセス後、ミューテックスを解放
  pthread_mutex_unlock(&mutex);

  // カウント値を出力
  printf("スレッド %ld のカウント値: %d\n", pthread_self(), count);
}

この方法は、スレッドストレージ期間を使用せずにスレッドセーフなコードを書くことができます。しかし、コードが複雑になり、デバッグが難しくなるという欠点があります。

スレッドローカルストレージライブラリを使用する

C言語には、スレッドストレージ期間の機能をより簡単に使用できるようにするライブラリがいくつかあります。例えば、libpthread-utils ライブラリには、__thread キーワードを使用してスレッドストレージ変数を宣言するためのマクロが提供されています。

#include <pthread-utils.h>

void* thread_function(void* arg) {
  // スレッドごとに異なるカウント値を保持する変数を宣言
  __thread int count = 0;

  // 10回カウントアップ
  for (int i = 0; i < 10; i++) {
    count++;
  }

  // カウント値を出力
  printf("スレッド %ld のカウント値: %d\n", pthread_self(), count);
}

この方法は、スレッドストレージ期間を使用するよりもシンプルで、カスタム同期メカニズムを使用するよりもコードが読みやすくなります。しかし、これらのライブラリは標準ライブラリの一部ではないため、すべてのプラットフォームで利用できるとは限りません。

最適な方法の選択

どの代替方法が最適かは、状況によって異なります。

  • 標準ライブラリに依存したくない場合は、スレッドローカルストレージライブラリを使用します。
  • 複雑な同期メカニズムを扱う必要なく、スレッドセーフなコードを書きたい場合は、カスタム同期メカニズムを使用します。
  • メモリ使用量を抑えたい場合は、スレッドストレージ期間を使用します。
  • シンプルで理解しやすいコードが必要な場合は、関数ローカル変数を使用します。
  • スレッドストレージ変数は、スレッドが終了しても自動的に解放されないことに注意する必要があります。必要に応じて、デストラクタを使用して変数を解放する必要があります。
  • 使用するコンパイラとライブラリによって、スレッドストレージ期間のサポート状況が異なる場合があります。