22

この質問から派生し、この質問関連しています

あるスレッドでオブジェクトを作成し、それへの参照/ポインターを別のスレッドに伝達する場合、他のスレッドが明示的なロック/メモリバリアなしでオブジェクトにアクセスするのは安全ではありませんか?

// thread 1
Obj obj;

anyLeagalTransferDevice.Send(&obj);
while(1); // never let obj go out of scope

// thread 2
anyLeagalTransferDevice.Get()->SomeFn();

あるいは、スレッドが触れたのすべてに関してメモリオーダリングを強制しないスレッド間でデータを伝達する合法的な方法はありますか?ハードウェアの観点からは、それが不可能であるべき理由はわかりません。

明確にするために; 問題は、キャッシュの一貫性、メモリの順序付けなどに関するものです。スレッド2のメモリのビューに、構築に関連する書き込みが含まれる前に、スレッド2はポインターを取得して使用できますobjか?Alexandrescu(?)の引用を間違えると、「悪意のあるCPU設計者とコンパイラー作成者が協力して、それを破る標準的な適合システムを構築できるでしょうか?」

4

5 に答える 5

17

スレッド セーフについての推論は難しい場合があり、私は C++11 メモリ モデルの専門家ではありません。幸いなことに、あなたの例は非常に単純です。コンストラクターが関係ないので、例を書き直します。

簡単な例

質問:次のコードは正しいですか? または、実行によって未定義の動作が発生する可能性がありますか?

// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();

// --- thread A ---
/* A1 */   int* pointer = receive();
/* A2 */   int answer = *pointer;

// --- thread B ---
           int answer;
/* B1 */   answer = 42;
/* B2 */   send(&answer);
           // wait forever

回答:のメモリ ロケーションでデータ競合が発生している可能性がありanswer、実行すると未定義の動作が発生します。詳細については、以下を参照してください。


データ転送の実装

もちろん、答えは関数sendとの可能な実装と合法的な実装に依存しreceiveます。次のデータ競合のない実装を使用します。アトミック変数は 1 つだけ使用され、すべてのメモリ操作は を使用することに注意してくださいstd::memory_order_relaxed。基本的に、これは、これらの関数がメモリの並べ替えを制限しないことを意味します。

std::atomic<int*> transfer{nullptr};

void send(int* pointer) {
    transfer.store(pointer, std::memory_order_relaxed);
}

int* receive() {
    while (transfer.load(std::memory_order_relaxed) == nullptr) { }
    return transfer.load(std::memory_order_relaxed);
}

メモリ操作の順序

マルチコア システムでは、スレッドは他のスレッドとは異なる順序でメモリの変更を確認できます。さらに、コンパイラと CPU の両方が、効率を高めるために 1 つのスレッド内でメモリ操作の順序を変更することがあります。これは常に行われます。のアトミック操作はstd::memory_order_relaxed同期に参加せず、順序付けも行いません。

上記の例では、コンパイラはスレッド B の操作を並べ替えて、B1 の前に B2 を実行することができます。これは、並べ替えがスレッド自体に影響を与えないためです。

// --- valid execution of operations in thread B ---
           int answer;
/* B2 */   send(&answer);
/* B1 */   answer = 42;
           // wait forever

データ競争

C++11 はデータ競合を次のように定義します (N3290 C++11 ドラフト):そのようなデータ競合は、未定義の動作を引き起こします。」また、「前に起こる」という用語は、同じドキュメントの前半で定義されています。

上記の例では、B1 と A2 は競合する非アトミック操作であり、どちらも先に発生しません。前のセクションで示したように、両方が同時に発生する可能性があることは明らかです。

それが C++11 で重要な唯一のことです。対照的に、Java メモリ モデルは、データ競合が発生した場合の動作を定義しようとしており、合理的な仕様を考え出すのにほぼ 10 年かかりました。C++11 は同じ過ちを犯しませんでした。


さらに詳しい情報

これらの基本があまり知られていないことに少し驚いています。決定的な情報源は、C++11 標準のマルチスレッド実行とデータ競合のセクションです。ただ、仕様がわかりにくい。

Hans Boehm の講演から始めるのが良いでしょう。たとえば、オンライン ビデオとして入手できます。

他にも多くの優れたリソースがありますが、他の場所で言及しました。

于 2012-04-20T18:55:50.110 に答える
3

同じデータへの並列アクセスはないため、問題はありません。

  • スレッド 1 が の実行を開始しObj::Obj()ます。
  • スレッド 1 は の実行を終了しObj::Obj()ます。
  • スレッド 1 は、占有されているメモリへの参照をobjスレッド 2 に渡します。
  • スレッド 1 は、そのメモリに対して他に何もしません (その後すぐに、無限ループに陥ります)。
  • スレッド 2 は、によって占有されているメモリへの参照を取得しobjます。
  • スレッド 2 は、まだ無限にループしているスレッド 1 に邪魔されずに、おそらくそれで何かを行います。

唯一の潜在的な問題はSend、メモリバリアとして機能しない場合ですが、実際には「合法的な転送デバイス」にはなりません。

于 2012-04-20T17:53:34.423 に答える
2

他の人がほのめかしたように、コンストラクターがスレッドセーフではない唯一の方法は、コンストラクターが終了する前に何らかの方法でポインターまたは参照を取得した場合であり、発生する唯一の方法は、コンストラクター自体に次のコードがある場合です。thisスレッド間で共有されるある種のコンテナへのポインタを登録します。

あなたの特定の例では、Branko Dimitrijevicがあなたのケースがどのようにうまくいっているかを完全に説明しました。しかし、一般的なケースでは、コンストラクターが終了するまで何かを使用しないように言いますが、コンストラクターが終了するまで発生しない「特別な」ことはないと思います。継承チェーンの (最後の) コンストラクターに入るまでに、オブジェクトは、すべてのメンバー変数が初期化されるなどして、ほぼ完全に「準備完了」になっています。したがって、他のクリティカル セクションの作業よりも悪くはありませんが、別のスレッドです。最初にそれについて知る必要があり、それが起こる唯一の方法はthis、コンストラクター自体で何らかの方法で共有している場合です。したがって、そうである場合は、「最後のこと」としてのみ行ってください。

于 2012-04-20T18:01:20.237 に答える
1

両方のスレッドを作成し、2 番目のスレッドがアクセスしている間に最初のスレッドがアクセスしていないことがわかっている場合にのみ、安全です。たとえば、参照/ポインターを渡した後、それを構築するスレッドがそれにアクセスしない場合は、問題ありません。それ以外の場合、スレッドは安全ではありません。データメンバーにアクセスするすべてのメソッド (読み取りまたは書き込み) がメモリをロックすることで、これを変更できます。

于 2012-04-20T17:47:11.017 に答える