C ++ 11
C ++ 11以降のバージョン:はい、このパターンは安全です。特に、関数ローカル静的変数の初期化はスレッドセーフであるため、上記のコードはスレッド間で安全に機能します。
これが実際に機能する方法は、コンパイラが関数自体に必要なボイラープレートを挿入して、アクセス前に変数が初期化されているかどうかを確認することです。ただし、、、にstd::mutex
実装されている場合、初期化された状態はすべてゼロであるため、明示的な初期化は必要ありません(変数はすべてゼロのセクションに存在するため、初期化は「無料」です)。アセンブリ1:gcc
clang
icc
.bss
inc(int& i):
mov eax, OFFSET FLAT:_ZL28__gthrw___pthread_key_createPjPFvPvE
test rax, rax
je .L2
push rbx
mov rbx, rdi
mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
call _ZL26__gthrw_pthread_mutex_lockP15pthread_mutex_t
test eax, eax
jne .L10
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
pop rbx
jmp _ZL28__gthrw_pthread_mutex_unlockP15pthread_mutex_t
.L2:
add DWORD PTR [rdi], 1
ret
.L10:
mov edi, eax
call _ZSt20__throw_system_errori
この行から開始すると、初期化せずに、関数ローカル静的mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
のアドレスをロードして呼び出すだけであることに注意してください。その前のコードは、明らかにpthreadsライブラリが存在するかどうかをチェックしているだけです。inc::mtx
pthread_mutex_lock
pthread_key_create
ただし、すべての実装がstd::mutex
すべてゼロとして実装されるという保証はありません。そのため、場合によっては、が初期化されているかどうかを確認するために、呼び出しごとに継続的なオーバーヘッドが発生する可能性がありmutex
ます。関数の外でミューテックスを宣言すると、それを回避できます。
これは、2つのアプローチを、インライン化できないコンストラクターを持つスタンドインクラスと対比する例です(したがって、コンパイラーは、初期状態がすべてゼロであると判断できません)。mutex2
#include <mutex>
class mutex2 {
public:
mutex2();
void lock();
void unlock();
};
void inc_local(int &i)
{
// Thread safe?
static mutex2 mtx;
std::unique_lock<mutex2> lock(mtx);
i++;
}
mutex2 g_mtx;
void inc_global(int &i)
{
std::unique_lock<mutex2> lock(g_mtx);
i++;
}
function-local versionは、(on gcc
)を次のようにコンパイルします。
inc_local(int& i):
push rbx
movzx eax, BYTE PTR _ZGVZ9inc_localRiE3mtx[rip]
mov rbx, rdi
test al, al
jne .L3
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_acquire
test eax, eax
jne .L12
.L3:
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
call _ZN6mutex24lockEv
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
pop rbx
jmp _ZN6mutex26unlockEv
.L12:
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
call _ZN6mutex2C1Ev
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_release
jmp .L3
mov rbx, rax
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_abort
mov rdi, rbx
call _Unwind_Resume
機能を扱う大量の定型文に注意してください__cxa_guard_*
。最初に、リップ相対フラグバイト_ZGVZ9inc_localRiE3mtx
2がチェックされ、ゼロ以外の場合、変数はすでに初期化されており、完了して高速パスに分類されます。x86では、ロードにはすでに必要な取得セマンティクスがあるため、不可分操作は必要ありません。
このチェックが失敗した場合、低速パスに進みます。これは基本的にダブルチェックされたロックの形式です。2つ以上のスレッドがここで競合している可能性があるため、初期チェックでは変数の初期化が必要であると判断できません。呼び出しはロックと2番目の__cxa_guard_acquire
チェックを実行し、高速パスにもフォールスルーするか(別のスレッドが同時にオブジェクトを初期化した場合)、またはで実際の初期化コードにジャンプする可能性があります.L12
。
jmp .L3
最後に、アセンブリの最後の5つの命令は、前に無条件で何もジャンプしないため、関数から直接到達できないことに注意してください。mutex2()
コンストラクターの呼び出しがある時点で例外をスローした場合に、例外ハンドラーによってジャンプされます。
全体として、ファーストアクセス初期化の実行時コストは低から中程度であると言えます。これは、高速パスが高価な命令なしで1バイトフラグのみをチェックするためです(関数の残りの部分は通常、少なくとも2つのアトミック操作を意味します。mutex.lock()
およびmutex.unlock()
、ただし、コードサイズが大幅に増加します。
グローバルバージョンと比較してください。これは、最初のアクセス前ではなくグローバル初期化中に初期化が行われることを除いて同じです。
inc_global(int& i):
push rbx
mov rbx, rdi
mov edi, OFFSET FLAT:g_mtx
call _ZN6mutex24lockEv
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:g_mtx
pop rbx
jmp _ZN6mutex26unlockEv
この関数は、初期化ボイラープレートがまったくない場合のサイズの3分の1未満です。
C++11より前
ただし、C ++ 11より前は、静的ローカルが初期化される方法についてコンパイラが特別な保証を行わない限り、これは一般的に安全ではありません。
少し前に、同様の問題を見ながら、この場合にVisualStudioによって生成されたアセンブリを調べました。メソッド用に生成されたアセンブリコードの擬似コードは、次のprint
ようになります。
void print(const std::string & s)
{
if (!init_check_print_mtx) {
init_check_print_mtx = true;
mtx.mutex(); // call mutex() ctor for mtx
}
// ... rest of method
}
はinit_check_print_mtx
、ローカル静的が初期化されているかどうかを追跡する、このメソッドに固有のコンパイラ生成グローバル変数です。この変数によって保護されている「1回限りの」初期化ブロック内では、ミューテックスが初期化される前に変数がtrueに設定されていることに注意してください。
このメソッドに競合する他のスレッドが初期化子をスキップして初期化されていないものを使用することを保証するため、これはばかげていますmtx
がmtx
、実際には、この方法で行うことで、無限再帰の問題を回避できます。std::mutex()
印刷にコールバックする場合に発生し、この動作は実際には標準で義務付けられています。
上記のNemoは、これがC ++ 11で修正され(より正確には再指定され)、すべてのレーシングスレッドを待機する必要があることを示しています。これにより、これは安全になりますが、準拠しているかどうかは独自のコンパイラを確認する必要があります。実際に新しい仕様にこの保証が含まれているかどうかは確認しませんでしたが、これがないマルチスレッド環境ではローカル統計がほとんど役に立たなかったことを考えると、まったく驚かないでしょう(おそらく、これがないプリミティブ値を除いて) .dataセグメント内のすでに初期化された場所を直接参照しているため、チェックアンドセットの動作。
1関数を、ロックされた領域の整数をインクリメントするだけprint()
の少し単純な関数に変更したことに注意してください。inc()
これは、元のロック構造と同じロック構造と意味を持ちますが、<<
演算子とを処理する一連のコードを回避しstd::cout
ます。
2c++filt
このデマングルを使用してguard variable for inc_local(int&)::mtx
。