スレッドセーフとは
マルチスレッド環境で、ライブラリやプログラム、クラスなどが複数のスレッドから同時に利用されても正常に動作すること
マルチスレッド環境では、一つのデータや手続きを同時に複数のスレッドが利用することがある
一方のスレッドがあるデータを操作したことで他のスレッドが利用するデータを破壊することがある
こうした環境を想定して正常に動作するよう設計されていることをスレッドセーフであるという
スレッドセーフの条件
- 静的またはグローバルな変数を、参照、操作してない
- グローバルな制限のあるリソース(ファイル、プロセスなど)を確保・解放していない
- 関数外で確保したメモリを、参照、操作してない
- スレッドセーフが保証されてない関数を呼んでいない
スレッドセーフのレベル
この関数はスレッドセーフである」といっても人それぞれとらえ方が違います。
「スレッドセーフとはこうである」という正式な文章がないため
「Effective Java」では「スレッドセーフ」の齟齬をなくすためスレッドセーフにレベルを持たせています。
レベル | 説明 |
---|---|
不変(immutable) | 値は状態を持たないので本質的にスレッドセーフ |
無条件スレッドセーフ(unconditionally thread-safe) | 値は状態を持つが、どのように使用されてもスレッドセーフ |
条件付きスレッドセーフ(conditionally thread-safe) | 値は状態を持ち、一部スレッドセーフであるが、スレッドセーフでない部分も存在する |
スレッドセーフでない(not thread-safe) | 複数のスレッドからアクセスされることを想定していない |
スレッド敵対(thread-hostile) | 全てのメソッドを同期的に使用してもスレッドセーフにならない ようはバグってる |
スレッドセーフにするには
リエントラント関数にする
ある関数を複数同時に実行した際、割り込まれたとしても、すべてのスレッドで結果が変わらない関数
また共通のメモリ(グローバル変数など)を破壊しないことが証明された関数
静的な変数、グローバル変数を保持しない関数
ダメな例(リエントラントではない)
int g_var = 1;
int f()
{
g_var = g_var + 2;
return g_var;
}
int g()
{
return f() + 2;
}
良い例(リエントラント)
int f(int i)
{
return i + 2;
}
int g(int i)
{
return f(i) + 2;
}
排他制御する
共有のメモリのアクセスを「逐次化」する
ミューテックスを用いてクリティカルセクション(同時にアクセスされると困る共有メモリ)を排他制御する
「mutex_lock」を用いた例
int g_var = 1;
int function(int i)
{
mutex_lock();
g_var = i;
mutex_unlock();
}
スレッド局所記憶を用いる
静的もしくは大域的なメモリをスレッドごとに局所的に使用するためのプログラミングの方法
例えばint型のグローバル変数を定義する際、生成するスレッド数分の配列にしておく
生成したスレッドに一意の値を振っておく
スレッドがグローバル変数を利用する際、自分に振られた一意の値の要素を利用する
こうすることでグローバル変数でありながら、スレッド固有の値を利用することができる
例
// スレッドの生成上限を100とする.
int g_val[100] = {0};
int get(int thread_no)
{
// 自分の要素の値を取得する.
return g_val[thread_no];
}
void set(int i, int thread_no)
{
// 自分の要素の値に保持する.
g_val[thread_no] = i;
}
アトミック操作にする
ロックはリソースへの同時アクセスを排他する操作
アトミック操作はメモリへの同時アクセスによるメモリ破壊を防ぐ操作
メモリ破壊されないが、プログラム上問題が起きるのであれば排他が必要
例
bool g_Flg= true;
int function(bool Flg)
{
//mutex_lock(); // bool値の変更は排他しなくてもメモリ破壊されないことが保障されている.
g_Flg = Flg;
//mutex_unlock(); // bool値の変更は排他しなくてもメモリ破壊されないことが保障されている.
}