web-dev-qa-db-ja.com

std :: shared_ptrスレッドセーフ

私はそれを読みました

「複数のスレッドは、オブジェクトが所有権を共有するコピーであっても、異なるshared_ptrオブジェクトを同時に読み書きできます。」 ( MSDN:標準C++ライブラリのスレッドセーフティ

それはshared_ptrオブジェクトの変更が安全であることを意味しますか?
インスタンスの場合、安全と見なされる次のコードは次のとおりです。

shared_ptr<myClass> global = make_shared<myClass>();
...

//In thread 1
shared_ptr<myClass> private = global;
...

//In thread 2
global = make_shared<myClass>();
...

その場合、スレッド1 privateglobalの元の値またはスレッド2が割り当てた新しい値を持っていることを確認できますが、いずれにしてもmyClassに有効なshared_ptrがありますか?

==編集==
私の動機を説明するだけです。構成を保持するための共有ポインターが必要であり、要求を処理するためのスレッドプールがあります。
so globalはグローバル構成です。
thread 1は、要求の処理を開始するときに現在の構成を取得しています。
thread 2は構成を更新しています。 (将来のリクエストにのみ適用)

うまくいけば、リクエスト処理の途中で中断することなく、そのように設定を更新できます。

43
Roee Gavirel

あなたが読んでいるものは、あなたがそれが意味すると思うものを意味していません。まず、 shared_ptr 自体のmsdnページを試してください。

[備考]セクションまで下にスクロールすると、問題の要点がわかります。基本的に、shared_ptr<>は「制御ブロック」を指します。これは、実際に「実」オブジェクトを指しているshared_ptr<>オブジェクトの数を追跡する方法です。したがって、これを行うと:

shared_ptr<int> ptr1 = make_shared<int>();

ここでmake_sharedを介してメモリを割り当てる呼び出しは1つだけですが、同じものを扱うべきではない2つの「論理」ブロックがあります。 1つは実際の値を格納するintで、もう1つはそれを機能させるすべてのshared_ptr<>「マジック」を格納する制御ブロックです。

スレッドセーフであるのは制御ブロック自体のみです。

私はそれを強調するために独自の行に入れました。 shared_ptrcontentsはスレッドセーフではなく、同じshared_ptrインスタンスへの書き込みもありません。ここに私が意味することを示すものがあります:

// In main()
shared_ptr<myClass> global_instance = make_shared<myClass>();
// (launch all other threads AFTER global_instance is fully constructed)

//In thread 1
shared_ptr<myClass> local_instance = global_instance;

これは問題ありません。実際、すべてのスレッドで必要なだけこれを行うことができます。そして、local_instanceが(スコープから出て)破棄されると、スレッドセーフにもなります。誰かがglobal_instanceにアクセスできても、違いはありません。 msdnから取得したスニペットは、基本的に「制御ブロックへのアクセスはスレッドセーフ」を意味するため、他のshared_ptr<>インスタンスを必要に応じて異なるスレッドで作成および破棄できます。

//In thread 1
local_instance = make_shared<myClass>();

これは結構です。それglobal_instanceオブジェクトに影響しますが(間接的にのみ)影響します。それが指す制御ブロックは減分されますが、スレッドセーフな方法で行われます。 local_instanceは、global_instanceが指すのと同じオブジェクト(または制御ブロック)を指しません。

//In thread 2
global_instance = make_shared<myClass>();

global_instanceが他のスレッド(アクセスしていると言っている)からアクセスされる場合、これはほぼ間違いなくうまくありません。これを行っている場合は、global_instanceが存在する場所に書き込みを行うため、ロックから読み取るだけでなく、ロックが必要です。したがって、複数のスレッドからのオブジェクトへの書き込みは、ロックによって保護されていない限り、不適切です。したがって、global_instanceオブジェクトから新しいshared_ptr<>オブジェクトを割り当てることでオブジェクトを読み取ることができますが、書き込むことはできません。

// In thread 3
*global_instance = 3;
int a = *global_instance;

// In thread 4
*global_instance = 7;

aの値は未定義です。 7であったり、3であったり、他の何かであったりします。 shared_ptr<>インスタンスのスレッドセーフは、相互に初期化されたshared_ptr<>インスタンスの管理にのみ適用され、それらが指しているものには適用されません。

私の言いたいことを強調するには、これを見てください:

shared_ptr<int> global_instance = make_shared<int>(0);

void thread_fcn();

int main(int argc, char** argv)
{
    thread thread1(thread_fcn);
    thread thread2(thread_fcn);
    ...
    thread thread10(thread_fcn);

    chrono::milliseconds duration(10000);
    this_thread::sleep_for(duration);

    return;
}

void thread_fcn()
{
    // This is thread-safe and will work fine, though it's useless.  Many
    // short-lived pointers will be created and destroyed.
    for(int i = 0; i < 10000; i++)
    {
        shared_ptr<int> temp = global_instance;
    }

    // This is not thread-safe.  While all the threads are the same, the
    // "final" value of this is almost certainly NOT going to be
    // number_of_threads*10000 = 100,000.  It'll be something else.
    for(int i = 0; i < 10000; i++)
    {
        *global_instance = *global_instance + 1;
    }
}

shared_ptr<>は、複数のオブジェクトの所有者が確実にオブジェクトの破棄を保証するメカニズムであり、複数のスレッドを保証するメカニズムではありませんオブジェクトに正しくアクセスできます。複数のスレッドで安全に使用するには、別の同期メカニズムが必要です( std :: mutex など)。

IMOについて考える最良の方法は、shared_ptr<>が同じメモリを指す複数のコピーがitselfの同期の問題を持たないことを保証するが、指し示されたオブジェクトに対して何でもします。そのように扱います。

85
Kevin Anderson

Kevinが書いたことに追加するために、C++ 14仕様にはshared_ptrオブジェクト自体へのアトミックアクセスの追加サポートがあります。

20.8.2.6 shared_ptrアトミックアクセス[util.smartptr.shared.atomic]

複数のスレッドからのshared_ptrオブジェクトへの同時アクセスは、アクセスがこのセクションの関数を介して排他的に行われ、インスタンスが最初の引数として渡される場合、データの競合を引き起こしません。

あなたがそうするなら:

//In thread 1
shared_ptr<myClass> private = atomic_load(&global);
...

//In thread 2
atomic_store(&global, make_shared<myClass>());
...

スレッドセーフになります。

23
Chris Dodd

有効なshared_ptr、および有効な参照カウント。

同じ変数への読み取り/割り当てを試みている2つのスレッド間の競合状態を説明しています。

これは一般に未定義の動作であるためです(個々のプログラムのコンテキストとタイミングでのみ意味があります)shared_ptrはそれを処理しません。

4
Yochai Timmer

読み取り操作はそれ自体のデータ競合の影響を受けないため、すべてのスレッドがconstメソッドのみ(これのコピーの作成を含む)を使用している限り、スレッド間でshared_ptrの同じインスタンスを共有しても安全です。あるスレッドが非constメソッドを使用すると(「別のオブジェクトをポイントする」など)、そのような使用はスレッドセーフではなくなります。

OPの例はスレッドセーフではないため、スレッドセーフにするにはスレッド1のアトミックロードとスレッド2のアトミックストア(C++ 11のセクション2.7.2.5)を使用する必要があります。

以前の回答で既に述べたように、MSDNテキストの重要なWordは確かに異なるshared_ptrオブジェクトです。

2
Leon

この質問に対するこれまでの回答は、説明されているシナリオに関して誤解を招くと思います。質問で説明されている非常によく似たシナリオがあります。他のすべてのスレッドには、現在の構成への読み取り専用アクセスのみが必要です。

// In thread n
shared_ptr<MyConfig> sp_local = sp_global;

これらのスレッドはいずれもMyConfigオブジェクトのコンテンツを変更しません。 sp_globalの参照カウントは、上記の行の実行ごとにインクリメントされます。

スレッド1は、定期的にsp_globalを構成の別のインスタンスにリセットします。

// In thread 1
shared_ptr<MyConfig> sp_global = make_shared<MyConfig>(new MyConfig);

これも安全なはずです。 sp_globalの参照カウントを1に戻し、sp_globalはすべての新しいローカルコピーと同様に、最新の構成を指すようになりました。したがって、ここで何も見逃していない場合、これはすべて完全にスレッドセーフである必要があります。

#include <iostream>
#include <memory>

using namespace std;

shared_ptr<int> sp1(new int(10));

int main()
{
    cout<<"Hello World! \n";

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "---------\n";

    shared_ptr<int> sp2 = sp1;
    shared_ptr<int>* psp3 = new shared_ptr<int>;
    *psp3 = sp1;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    sp1.reset(new int(20));

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    delete psp3;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "---------\n";

    sp1 = nullptr;

    cout << "sp1 use count: " << sp1.use_count() << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";

    return 0;
}

そして出力

Hello World!
sp1 use count: 1, sp1: 10
---------
sp1 use count: 3, sp1: 10
sp2 use count: 3, sp2: 10
sp3 use count: 3, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 2, sp2: 10
sp3 use count: 2, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 1, sp2: 10
---------
sp1 use count: 0
sp2 use count: 1, sp2: 10
1
hagh

これは、shared_ptrのスレッドセーフに関する私の理解です。 IMO、shared_ptrのスレッドセーフに関しては3つの側面があります。

最初のものはshared_ptr自体です。 shared_ptr自体はスレッドセーフではないと言います。つまり、複数のスレッドで、oneshared_ptrオブジェクトとアクセスは書き込み中です。たとえば、次の状況でデータ競合が発生します。

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
global_ptr.reset();

2番目の側面は、shared_ptrの内部構造です。スレッドセーフだと思います。その結果、multipleshared_ptrオブジェクトにアクセスするときにデータの競合はなく、オブジェクトは同じ管理対象オブジェクトを指します。たとえば、次の状況ではデータの競合はありません。

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
local_ptr.reset();

3番目の側面は、shared_ptrの管理対象オブジェクトがスレッドセーフである場合とそうでない場合があることです。たとえば、次の状況ではデータの競合が発生していると言えます。

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
(*local_ptr).clear();

https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html#shared_ptr.thread

https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic

0
Lujun Weng