C ++ 11でthread_localはどういう意味ですか?


131

thread_localC ++ 11 での説明と混同しています。私の理解では、各スレッドには関数内のローカル変数の一意のコピーがあります。グローバル/静的変数は、すべてのスレッドからアクセスできます(ロックを使用して同期アクセスされる場合があります)。そして、thread_local変数はすべてのスレッドに表示されますが、それらが定義されているスレッドによってのみ変更できますか?それが正しいか?

回答:


150

スレッドローカルストレージ期間は、グローバルまたは静的なストレージ期間(それを使用する関数の観点から)のように見えるデータを指すために使用される用語ですが、実際には、スレッドごとに1つのコピーがあります。

現在の自動(ブロック/関数中に存在)、静的(プログラム期間中に存在)、動的(割り当てと割り当て解除の間のヒープに存在)に追加されます。

スレッドローカルなものは、スレッドの作成時に存在し、スレッドが停止すると破棄されます。

次にいくつかの例を示します。

シードをスレッドごとに維持する必要がある乱数ジェネレータを考えてみます。スレッドローカルシードを使用すると、各スレッドは、他のスレッドとは関係なく、独自の乱数シーケンスを取得します。

シードがランダム関数内のローカル変数である場合、それを呼び出すたびに初期化され、毎回同じ数が得られます。グローバルの場合、スレッドは互いのシーケンスを妨害します。

別の例はstrtok、トークン化の状態がスレッド固有のベースで格納されるようなものです。このようにして、単一のスレッドは、他のスレッドがトークン化の作業を台無しにせず、複数の呼び出しに対して状態を維持できることを確認できます。これにより、strtok基本的にstrtok_r(スレッドセーフバージョン)が冗長になります。

これらの例はどちらも、スレッドローカル変数がそれを使用する関数に存在できるようにします。事前にスレッド化されたコードでは、関数内の静的ストレージ期間変数にすぎません。スレッドの場合、スレッドのローカルストレージ期間に変更されます。

さらに別の例は、のようなものですerrnoerrno呼び出しの1つが失敗した後で、変数をチェックする前に、個別のスレッドを変更する必要はありませんが、スレッドごとに1つのコピーのみが必要です。

このサイトには、さまざまなストレージ期間指定子の合理的な説明があります。


4
スレッドローカルを使用しても、の問題は解決しませんstrtokstrtokシングルスレッド環境でも壊れます。
James Kanze、2012

11
すみません、言い換えさせてください。strtokで新しい問題が発生することはありません:-)
paxdiablo

7
実際、r「リエントラント」の略で、スレッドの安全性とは関係ありません。スレッドローカルストレージを使用してスレッドセーフに動作するものを作成できることは事実ですが、それらを再入可能にすることはできません。
Kerrek SB 2012

5
シングルスレッド環境では、関数が呼び出しグラフのサイクルの一部である場合にのみ、関数を再入可能にする必要があります。リーフ関数(他の関数を呼び出さない関数)は、定義上、サイクルの一部ではなく、strtok他の関数を呼び出す必要がある理由はありません。
MSalters 2012

3
これは台無しにしてしまいます: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss 2014年

135

変数を宣言するとthread_local、各スレッドには独自のコピーがあります。名前で参照すると、現在のスレッドに関連付けられているコピーが使用されます。例えば

thread_local int i=0;

void f(int newval){
    i=newval;
}

void g(){
    std::cout<<i;
}

void threadfunc(int id){
    f(id);
    ++i;
    g();
}

int main(){
    i=9;
    std::thread t1(threadfunc,1);
    std::thread t2(threadfunc,2);
    std::thread t3(threadfunc,3);

    t1.join();
    t2.join();
    t3.join();
    std::cout<<i<<std::endl;
}

このコードは、「2349」、「3249」、「4239」、「4329」、「2439」、または「3429」を出力しますが、それ以外は何も出力しません。各スレッドには、の独自のコピーがありi、割り当てられ、インクリメントされてから出力されます。実行中のスレッドにmainも独自のコピーがあり、最初に割り当てられ、変更されません。これらのコピーは完全に独立しており、それぞれに異なるアドレスがあります。

その点で特別なのは名前だけです--- thread_local変数のアドレスを取得する場合、通常のオブジェクトへの通常のポインタがあり、スレッド間で自由に渡すことができます。例えば

thread_local int i=0;

void thread_func(int*p){
    *p=42;
}

int main(){
    i=9;
    std::thread t(thread_func,&i);
    t.join();
    std::cout<<i<<std::endl;
}

のアドレスがiスレッド関数に渡されるiため、メインスレッドに属しているのコピーをに割り当てることができますthread_local。したがって、このプログラムは「42」を出力します。これを行う場合は、それが*p属するスレッドが終了した後にアクセスされないように注意する必要があります。そうでない場合は、ポイントされたオブジェクトが破棄される他の場合と同様に、ぶら下がりポインタと未定義の動作が発生します。

thread_local変数は「最初に使用する前に」初期化されるため、特定のスレッドによって変更されない場合は、必ずしも初期化されるとは限りません。これは、thread_local完全に自己完結型であり、それらのどれにも触れないスレッドに対して、コンパイラーがプログラム内のすべての変数を作成することを回避できるようにするためです。例えば

struct my_class{
    my_class(){
        std::cout<<"hello";
    }
    ~my_class(){
        std::cout<<"goodbye";
    }
};

void f(){
    thread_local my_class unused;
}

void do_nothing(){}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}

このプログラムには、メインスレッドと手動で作成したスレッドの2つのスレッドがあります。どちらのスレッドもを呼び出さないfため、thread_localオブジェクトは使用されません。したがって、コンパイラーがのインスタンスを0、1、または2のどちらで構築するかは指定されてmy_classおらず、出力は「」、「hellohellogoodbyegoodbye」または「hellogoodbye」のいずれかになります。


1
変数のスレッドローカルコピーは、新しく初期化された変数のコピーであることに注意することが重要だと思います。あなたが追加した場合つまり、g()の初めに電話をthreadFuncし、出力は次のようになります0304029か、ペアの他のいくつかの並べ替え020304。つまりi、スレッドが作成される前に9が割り当てられていても、スレッドはiwhereの新しく作成されたコピーを取得しi=0ます。場合iに付与されthread_local int i = random_integer()、その後、各スレッドは、新しいランダムな整数を取得します。
Mark H

ない正確の順列は020304、のような他の配列があるかもしれません020043
Hongxu陳

私が見つけた興味深い一口:GCCは、thread_local変数のアドレスをテンプレート引数として使用することをサポートしていますが、他のコンパイラーはサポートしていません(これを書いている時点では、clang、vstudioを試しました)。それについて規格が何を言っているのか、またはこれが不特定の領域であるかどうかはわかりません。
jwd

23

スレッドローカルストレージは、静的(=グローバル)ストレージのようなあらゆる面で、各スレッドがオブジェクトの個別のコピーを持っているという点だけが異なります。オブジェクトのライフタイムは、スレッドの開始時(グローバル変数の場合)または最初の初期化時(ブロックローカル静的の場合)に始まり、スレッドが終了すると(つまり、join()呼び出されると)終了します。

その結果、宣言staticすることもできるthread_local変数、つまりグローバル変数(より正確には、「名前空間スコープ」の変数)、静的クラスメンバー、およびブロック静的変数(static暗黙の場合)として宣言できます。

例として、スレッドプールがあり、作業負荷がどの程度適切に分散されているかを知りたいとします。

thread_local Counter c;

void do_work()
{
    c.increment();
    // ...
}

int main()
{
    std::thread t(do_work);   // your thread-pool would go here
    t.join();
}

これは、たとえば次のような実装で、スレッド使用統計を出力します。

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.