41

print次のプログラムでは、関数ローカルのミューテックス オブジェクトを使用して関数をスレッド セーフにしようとしています。

#include <iostream>
#include <chrono>
#include <mutex>
#include <string>
#include <thread>


void print(const std::string & s)
{    
    // Thread safe?
    static std::mutex mtx;
    std::unique_lock<std::mutex> lock(mtx);
    std::cout <<s << std::endl;
}


int main()
{
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("a" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("b" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("c" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("d" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("e" + std::to_string(i)); }).detach();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

これは安全ですか?

私の疑問 は、同様のケースを提示するこの質問から生じます。

4

3 に答える 3

25

C ++ 11

C ++ 11以降のバージョン:はい、このパターンは安全です。特に、関数ローカル静的変数の初期化はスレッドセーフであるため、上記のコードはスレッド間で安全に機能します。

これが実際に機能する方法は、コンパイラが関数自体に必要なボイラープレートを挿入して、アクセス前に変数が初期化されているかどうかを確認することです。ただし、、、にstd::mutex実装されている場合、初期化された状態はすべてゼロであるため、明示的な初期化は必要ありません(変数はすべてゼロのセクションに存在するため、初期化は「無料」です)。アセンブリ1gccclangicc.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::mtxpthread_mutex_lockpthread_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_localRiE3mtx2がチェックされ、ゼロ以外の場合、変数はすでに初期化されており、完了して高速パスに分類されます。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に設定されていることに注意してください。

このメソッドに競合する他のスレッドが初期化子をスキップして初期化されていないものを使用することを保証するため、これはばかげていますmtxmtx、実際には、この方法で行うことで、無限再帰の問題を回避できます。std::mutex()印刷にコールバックする場合に発生し、この動作は実際には標準で義務付けられています。

上記のNemoは、これがC ++ 11で修正され(より正確には再指定され)、すべてのレーシングスレッドを待機する必要があることを示しています。これにより、これは安全になりますが、準拠しているかどうかは独自のコンパイラを確認する必要があります。実際に新しい仕様にこの保証が含まれているかどうかは確認しませんでしたが、これがないマルチスレッド環境ではローカル統計がほとんど役に立たなかったことを考えると、まったく驚かないでしょう(おそらく、これがないプリミティブ値を除いて) .dataセグメント内のすでに初期化された場所を直接参照しているため、チェックアンドセットの動作。


1関数を、ロックされた領域の整数をインクリメントするだけprint()の少し単純な関数に変更したことに注意してください。inc()これは、元のロック構造と同じロック構造と意味を持ちますが、<<演算子とを処理する一連のコードを回避しstd::coutます。

2c++filtこのデマングルを使用してguard variable for inc_local(int&)::mtx

于 2013-01-01T02:39:58.173 に答える
16

これは、いくつかの理由から、リンクされた質問と同じではありません。

リンクされた質問はC++ 11ではありませんが、あなたのものです。C++11 では、関数ローカルの静的変数の初期化は常に安全です。C++11 より前は、GCC や Clang のデフォルトでスレッドセーフな初期化など、一部のコンパイラでのみ安全でした。

リンクされた質問は、関数を呼び出すことによって参照を初期化します。これは動的な初期化であり、実行時に発生します。のデフォルトのコンストラクターstd::mutexconstexpr、静的変数が一定の初期化を持つようにするためです。つまり、ミューテックスはコンパイル時 (またはリンク時) に初期化できるため、実行時に動的に行うことは何もありません。複数のスレッドが関数を同時に呼び出す場合でも、ミューテックスを使用する前に実際に行う必要はありません。

コードは安全です (コンパイラが C++11 ルールを正しく実装していると仮定します)。

于 2013-01-01T16:08:46.423 に答える
6

ミューテックスが静的である限り、はい。

ローカルの非静的は、間違いなく安全ではありません。すべてのスレッドが同じスタックを使用しない限り、つまり、1つのセルが同時に多くの異なる値を保持できるメモリを発明し、ノーベル委員会が次のノーベル賞を通知するのを待っているだけです。

ミューテックス用に、ある種の「グローバル」(共有)メモリスペースが必要です。

于 2012-12-31T23:00:31.947 に答える