2093

C ++ 11は標準化されたメモリモデルを導入しましたが、それは正確にはどういう意味ですか?そして、それはC ++プログラミングにどのように影響しますか?

この記事ハーブサッターを引用しているGavin Clarkeによる)は、次のように述べています。

メモリモデルは、C ++コードに、誰がコンパイラを作成し、どのプラットフォームでコンパイラを実行しているかに関係なく呼び出す標準化されたライブラリがあることを意味します。さまざまなスレッドがプロセッサのメモリと通信する方法を制御する標準的な方法があります。

「標準に含まれるさまざまなコア間で[コード]を分割することについて話しているときは、メモリモデルについて話している。人々がコードで行う次の仮定を破ることなく、それを最適化する」とサッター氏は述べた。

ええと、私はオンラインで利用できるこの段落と同様の段落を覚えることができ(私は生まれてから自分の記憶モデルを持っていたので:P)、他の人からの質問への回答として投稿することもできますが、正直なところ、私は正確には理解していませんこれ。

C ++プログラマーは以前からマルチスレッドアプリケーションを開発していましたが、それがPOSIXスレッド、Windowsスレッド、またはC ++ 11スレッドであるかどうかはどのように重要ですか?メリットは何ですか?低レベルの詳細を理解したい。

また、C ++ 11メモリモデルは、C ++ 11マルチスレッドのサポートに何らかの形で関連していると感じます。これは、これら2つを一緒に見ることがよくあるためです。もしそうなら、どのくらい正確に?なぜそれらは関連している必要がありますか?

マルチスレッドの内部がどのように機能するのか、そしてメモリモデルが一般的に何を意味するのかわからないので、これらの概念を理解するのを手伝ってください。:-)

4

8 に答える 8

2442

まず、あなたは言語弁護士のように考えることを学ぶ必要があります。

C ++仕様は、特定のコンパイラ、オペレーティングシステム、またはCPUを参照していません。これは、実際のシステムを一般化した抽象マシンを参照しています。言語弁護士の世界では、プログラマーの仕事は抽象マシンのコードを書くことです。コンパイラの仕事は、具体的なマシンでそのコードを実現することです。仕様に厳密にコーディングすることで、現在または50年後を問わず、準拠したC++コンパイラを備えたシステムでコードを変更せずにコンパイルおよび実行できるようになります。

C ++ 98 / C ++ 03仕様の抽象マシンは、基本的にシングルスレッドです。そのため、仕様に関して「完全に移植可能な」マルチスレッドC++コードを作成することはできません。この仕様では、メモリのロードとストアのアトミック性や、ロードとストアが発生する可能性のある順序については何も述べられていません。ミューテックスなどは気にしないでください。

もちろん、実際には、pthreadやWindowsなどの特定の具象システム用にマルチスレッドコードを記述できます。ただし、C ++ 98 / C++03のマルチスレッドコードを作成する標準的な方法はありません。

C ++ 11の抽象マシンは、設計によりマルチスレッド化されています。また、明確に定義されたメモリモデルがあります。つまり、メモリへのアクセスに関して、コンパイラが実行できることと実行しないことを示しています。

次の例を考えてみましょう。ここでは、グローバル変数のペアが2つのスレッドによって同時にアクセスされます。

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

スレッド2は何を出力する可能性がありますか?

C ++ 98 / C ++ 03では、これは未定義動作でさえありません。標準は「スレッド」と呼ばれるものを想定していないため、質問自体は無意味です。

C ++ 11では、ロードとストアは一般にアトミックである必要がないため、結果は未定義動作になります。これはあまり改善されていないように見えるかもしれません...そしてそれ自体はそうではありません。

しかし、C ++ 11では、次のように書くことができます。

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

今、物事ははるかに興味深いものになります。まず、ここでの動作を定義します。スレッド2は、(0 0スレッド1の前に実行される37 17場合)、(スレッド1の後に実行される場合)、または0 17(スレッド1がxに割り当てられた後、yに割り当てられる前に実行される場合)を出力できるようになりました。

37 0C ++ 11のアトミックロード/ストアのデフォルトモードは逐次一貫性を強制することであるため、出力できないのはです。つまり、すべてのロードとストアは、各スレッド内で記述した順序で発生したかのように「あたかも」発生する必要がありますが、スレッド間の操作は、システムが好きなようにインターリーブできます。したがって、アトミックのデフォルトの動作は、ロードとストアのアトミック性順序の両方を提供します。

現在、最新のCPUでは、逐次一貫性の確保にはコストがかかる可能性があります。特に、コンパイラは、ここでのすべてのアクセスの間に本格的なメモリバリアを放出する可能性があります。ただし、アルゴリズムが順不同のロードとストアを許容できる場合は、つまり、原子性が必要であるが順序付けは必要ない場合。つまり、37 0このプログラムからの出力として許容できる場合は、次のように記述できます。

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

CPUが最新であるほど、前の例よりも高速になる可能性が高くなります。

最後に、特定のロードとストアを順番に保持する必要がある場合は、次のように記述できます。

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

これにより、順序付けられたロードとストアに戻ることが37 0できます。これにより、出力は可能ではなくなりますが、最小限のオーバーヘッドで実行できます。(この些細な例では、結果は本格的な逐次一貫性と同じです。より大きなプログラムでは、そうではありません。)

もちろん、表示したい出力が0 0また37 17はである場合は、元のコードの周りにミューテックスをラップするだけです。しかし、これまで読んだことがあれば、それがどのように機能するかをすでに知っていると思います。この答えは、私が意図したよりもすでに長くなっています:-)。

つまり、収益です。ミューテックスは素晴らしく、C++11はそれらを標準化します。ただし、パフォーマンス上の理由から、低レベルのプリミティブ(たとえば、従来のダブルチェックロックパターン)が必要な場合があります。新しい標準は、ミューテックスや条件変数などの高レベルのガジェットを提供し、アトミックタイプやさまざまなフレーバーのメモリバリアなどの低レベルのガジェットも提供します。これで、完全に標準で指定された言語内で洗練された高性能の並行ルーチンを記述でき、コードが現在のシステムと将来のシステムの両方で変更されずにコンパイルおよび実行されることを確認できます。

率直に言って、あなたが専門家であり、深刻な低レベルのコードに取り組んでいない限り、おそらくミューテックスと条件変数に固執する必要があります。それが私がやろうとしていることです。

この内容の詳細については、このブログ投稿を参照してください。

于 2011-06-12T00:23:46.617 に答える
375

メモリ整合性モデル(または略してメモリモデル)を理解するためのアナロジーを示します。これは、レスリー・ランポートの独創的な論文「分散システムにおける時間、時計、およびイベントの順序付け」に触発されています。類推は適切であり、基本的な意味がありますが、多くの人にとってはやり過ぎかもしれません。しかし、それが記憶の一貫性モデルについての推論を容易にする精神的なイメージ(絵画的表現)を提供することを願っています。

横軸がアドレス空間を表し(つまり、各メモリ位置がその軸上の点で表される)、縦軸が時間を表す時空図ですべてのメモリ位置の履歴を見てみましょう(これから、一般に、時間の普遍的な概念はありません)。したがって、各メモリ位置に保持されている値の履歴は、そのメモリアドレスの垂直列で表されます。それぞれの値の変更は、スレッドの1つがその場所に新しい値を書き込んでいるためです。メモリイメージとは、特定のスレッドによって特定の時間に監視可能なすべてのメモリ位置の値の集約/組み合わせを意味します。

「メモリの一貫性とキャッシュの一貫性に関する入門書」からの引用

直感的な(そして最も制限的な)メモリモデルは逐次一貫性(SC)であり、マルチスレッド実行は、スレッドがシングルコアプロセッサで時分割多重化されているかのように、各構成スレッドの順次実行のインターリーブのように見えます。

そのグローバルメモリの順序は、プログラムの実行ごとに異なる可能性があり、事前にわからない場合があります。SCの特徴は、同時性の平面(つまり、メモリイメージ)を表すアドレス空間時間図の水平スライスのセットです。特定のプレーンでは、そのすべてのイベント(またはメモリ値)が同時に発生します。絶対時間の概念があり、すべてのスレッドがどのメモリ値が同時であるかについて合意します。SCでは、常に、すべてのスレッドで共有されるメモリイメージは1つだけです。つまり、すべての時点で、すべてのプロセッサがメモリイメージ(つまり、メモリの集約コンテンツ)に同意します。これは、すべてのスレッドがすべてのメモリ位置に対して同じ値のシーケンスを表示するだけでなく、すべてのプロセッサが同じ値を監視することを意味します。すべての変数の値の組み合わせ。これは、(すべてのメモリ位置での)すべてのメモリ操作がすべてのスレッドによって同じ合計順序で監視されると言うのと同じです。

緩和されたメモリモデルでは、各スレッドは独自の方法でアドレス空間をスライスします。唯一の制限は、すべてのスレッドがすべての個々のメモリ位置の履歴に同意する必要があるため、各スレッドのスライスが互いに交差してはならないことです(もちろん、異なるスレッドのスライスは、互いに交差する可能性があり、交差する可能性があります)。それをスライスする普遍的な方法はありません(アドレス空間時間の特権的なフォリエーションはありません)。スライスは平面(または線形)である必要はありません。それらは湾曲している可能性があり、これにより、スレッドは、別のスレッドによって書き込まれた値を、書き込まれた順序から外れて読み取ることができます。特定のスレッドで表示すると、異なるメモリ位置の履歴が相互に任意にスライド(または引き伸ばされる)する可能性があります。。各スレッドは、どのイベント(または同等にメモリ値)が同時に発生するかについて異なる意味を持ちます。あるスレッドと同時に発生する一連のイベント(またはメモリ値)は、別のスレッドと同時に発生することはありません。したがって、緩和されたメモリモデルでは、すべてのスレッドが各メモリ位置について同じ履歴(つまり、値のシーケンス)を引き続き監視します。ただし、異なるメモリイメージ(つまり、すべてのメモリ位置の値の組み合わせ)を観察する場合があります。2つの異なるメモリ位置が同じスレッドによって順番に書き込まれる場合でも、新しく書き込まれた2つの値は、他のスレッドによって異なる順序で監視される可能性があります。

[ウィキペディアからの写真] ウィキペディアからの画像

アインシュタインの特殊相対性理論に精通している読者は、私がほのめかしていることに気付くでしょう。ミンコウスキーの言葉をメモリモデルの領域に変換する:アドレス空間と時間は、アドレス空間時間の影です。この場合、各オブザーバー(つまり、スレッド)は、イベントのシャドウ(つまり、メモリストア/ロード)を自分の世界線(つまり、時間軸)と自分の同時性の平面(アドレス空間軸)に投影します。 。C ++ 11メモリモデルのスレッドは、特殊相対性理論で相互に相対的に移動しているオブザーバーに対応します。逐次一貫性は、ガリラヤ時空に対応します(つまり、すべての観測者は、イベントの1つの絶対的な順序とグローバルな同時性の感覚に同意します)。

メモリモデルと特殊相対性理論の類似性は、両方が部分的に順序付けられたイベントのセットを定義するという事実に由来します。これは、しばしば因果集合と呼ばれます。一部のイベント(つまり、メモリストア)は、他のイベントに影響を与える可能性があります(ただし、影響を受けません)。C ++ 11スレッド(または物理学のオブザーバー)は、イベントのチェーン(つまり、完全に順序付けられたセット)にすぎません(たとえば、メモリのロードと、場合によっては異なるアドレスへの格納)。

相対性理論では、すべての観測者が同意する唯一の時間的順序は「時間的」イベント(つまり、粒子が遅くなることによって原則的に接続可能なイベント)間の順序であるため、一部の順序は半順序イベントの一見混沌とした図に復元されます真空中の光速よりも)。時系列に関連するイベントのみが常に順序付けられます。 物理学の時間、クレイグ・カレンダー

C ++ 11メモリモデルでは、同様のメカニズム(取得-解放整合性モデル)を使用して、これらのローカル因果関係を確立します。

メモリの一貫性の定義とSCを放棄する動機を提供するために、「メモリの一貫性とキャッシュの一貫性に関する入門書」から引用します。

共有メモリマシンの場合、メモリ整合性モデルは、そのメモリシステムのアーキテクチャ上目に見える動作を定義します。シングルプロセッサコアの正当性基準は、「1つの正しい結果」と「多くの誤った選択肢」の間で動作を分割します。これは、プロセッサのアーキテクチャが、スレッドの実行により、順序が正しくないコアであっても、特定の入力状態を単一の明確に定義された出力状態に変換することを義務付けているためです。ただし、共有メモリの整合性モデルは、複数のスレッドのロードとストアに関係し、通常、多くの正しい実行を可能にします多くの(より多くの)間違ったものを禁止している間。複数の正しい実行の可能性は、ISAが複数のスレッドを同時に実行できるようにするためであり、多くの場合、異なるスレッドからの命令が合法的にインターリーブされる可能性があります。

リラックスまたは弱いメモリ整合性モデルは、強力なモデルのほとんどのメモリ順序が不要であるという事実によって動機付けられています。スレッドが10個のデータ項目を更新してから同期フラグを更新する場合、プログラマーは通常、データ項目が相互に順番に更新されるかどうかは気にせず、フラグが更新される前にすべてのデータ項目が更新されることだけを気にします(通常はFENCE命令を使用して実装されます)。 )。緩和されたモデルは、この増加した順序付けの柔軟性をキャプチャし、SCのより高いパフォーマンスと正確さの両方を得るためにプログラマーが「<em>必要とする」順序のみを保持しようとします。たとえば、特定のアーキテクチャでは、FIFO書き込みバッファは、キャッシュに結果を書き込む前に、コミットされた(リタイアされた)ストアの結果を保持するために各コアによって使用されます。この最適化はパフォーマンスを向上させますが、SCに違反します。書き込みバッファは、ストアミスの処理のレイテンシを隠します。店舗は一般的であるため、ほとんどの店舗でストールを回避できることは重要なメリットです。シングルコアプロセッサの場合、Aへの1つ以上のストアが書き込みバッファにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファをアーキテクチャ上非表示にすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。店舗は一般的であるため、ほとんどの店舗でストールを回避できることは重要なメリットです。シングルコアプロセッサの場合、Aへの1つ以上のストアが書き込みバッファにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファをアーキテクチャ上非表示にすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。店舗は一般的であるため、ほとんどの店舗でストールを回避できることは重要なメリットです。シングルコアプロセッサの場合、Aへの1つ以上のストアが書き込みバッファにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファをアーキテクチャ上非表示にすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。シングルコアプロセッサの場合、Aへの1つ以上のストアが書き込みバッファにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファをアーキテクチャ上非表示にすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。シングルコアプロセッサの場合、Aへの1つ以上のストアが書き込みバッファにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファをアーキテクチャ上非表示にすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。Aへの1つ以上のストアが書き込みバッファーにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファーをアーキテクチャーから見えなくすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。Aへの1つ以上のストアが書き込みバッファーにある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファーをアーキテクチャーから見えなくすることができます。これは通常、Aへの最新のストアの値をAからのロードにバイパスすることによって行われます。ここで、「最新」はプログラムの順序によって決定されます。または、Aへのストアが書き込みバッファにある場合はAのロードをストールします。 。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。または、Aへのストアが書き込みバッファにある場合は、Aのロードをストールします。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。または、Aへのストアが書き込みバッファにある場合は、Aのロードをストールします。複数のコアを使用する場合、それぞれに独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではなく、マルチコアプロセッサでアーキテクチャ的に書き込みバッファを表示できます。

ストアとストアの並べ替えは、コアに非FIFO書き込みバッファーがあり、ストアが入力された順序とは異なる順序で出発できる場合に発生する可能性があります。これは、2番目のストアがヒットしている間に最初のストアがキャッシュでミスした場合、または2番目のストアが以前のストアと合体できる場合(つまり、最初のストアの前)に発生する可能性があります。ロードロードの並べ替えは、プログラムの順序から外れた命令を実行する動的にスケジュールされたコアでも発生する可能性があります。これは、別のコアでストアを並べ替えるのと同じように動作します(2つのスレッド間でインターリーブする例を思い付くことができますか?)。以前のロードを後のストアに並べ替える(ロードストアの並べ替え)と、保護するロックを解除した後に値をロードするなど、多くの誤った動作が発生する可能性があります(ストアがロック解除操作の場合)。

キャッシュの一貫性とメモリの一貫性が混同されることがあるため、次の引用も参考にしてください。

一貫性とは異なり、キャッシュコヒーレンスはソフトウェアからは見えず、必要でもありません。Coherenceは、共有メモリシステムのキャッシュを、シングルコアシステムのキャッシュと同じように機能的に見えないようにすることを目指しています。正しいコヒーレンスにより、プログラマーは、ロードとストアの結果を分析することによって、システムにキャッシュがあるかどうか、どこにあるかを判断できなくなります。これは、正しいコヒーレンスにより、キャッシュが新しい機能や異なる機能の動作を決して有効にしないことが保証されるためです(プログラマーはタイミングを使用して可能性のあるキャッシュ構造を推測できる場合があります)情報)。キャッシュコヒーレンスプロトコルの主な目的は、すべてのメモリ位置に対してシングルライター-マルチリーダー(SWMR)を不変に維持することです。コヒーレンスと整合性の重要な違いは、コヒーレンスはメモリ位置ごとに指定されるのに対し、整合性はすべてのメモリ位置に関して指定されることです。

私たちの心の絵を続けると、SWMR不変量は、任意の1つの場所に最大で1つの粒子が存在するという物理的要件に対応しますが、任意の場所に無制限の数のオブザーバーが存在する可能性があります。

于 2013-08-29T20:42:27.170 に答える
135

これは今では数年前の質問ですが、非常に人気があるので、C++11メモリモデルについて学ぶための素晴らしいリソースに言及する価値があります。これをさらに完全な答えにするために彼の話を要約しても意味がありませんが、これが実際に標準を書いた人であることを考えると、話を見る価値は十分にあると思います。

Herb Sutterは、Channel9サイト(パート1およびパート2 )で入手可能な「atomic<>Weapons」というタイトルのC++11メモリモデルについて3時間の長い話をしています。講演はかなり技術的で、次のトピックをカバーしています。

  1. 最適化、人種、およびメモリモデル
  2. 注文–内容:取得してリリース
  3. 順序付け–方法:ミューテックス、アトミック、および/またはフェンス
  4. コンパイラとハードウェアに関するその他の制限
  5. コードの生成とパフォーマンス:x86 / x64、IA64、POWER、ARM
  6. リラックスしたアトミック

話はAPIについて詳しく説明していませんが、推論、背景、内部および舞台裏について詳しく説明しています(POWERとARMが同期ロードを効率的にサポートしていないという理由だけで、リラックスしたセマンティクスが標準に追加されたことをご存知ですか?)。

于 2013-12-20T13:22:43.583 に答える
80

これは、標準がマルチスレッドを定義し、複数のスレッドのコンテキストで何が起こるかを定義することを意味します。もちろん、人々はさまざまな実装を使用していましたが、それはstd::string、私たち全員がホームロールstringクラスを使用できるのに、なぜ必要なのかを尋ねるようなものです。

POSIXスレッドまたはWindowsスレッドについて話している場合、同時に実行するハードウェア関数であるため、実際にはx86スレッドについて話しているので、これは少し幻想です。C ++ 0xメモリモデルは、x86、ARM、MIPS、またはその他の考えられるものに関係なく、保証を提供します。

于 2011-06-11T23:42:51.893 に答える
60

メモリモデルを指定しない言語の場合、プロセッサアーキテクチャで指定された言語とメモリモデルのコードを記述します。プロセッサは、パフォーマンスのためにメモリアクセスを並べ替えることを選択できます。したがって、プログラムにデータ競合がある場合(データ競合とは、複数のコア/ハイパースレッドが同じメモリに同時にアクセスできる場合です)、プロセッサメモリモデルに依存しているため、プログラムはクロスプラットフォームではありません。プロセッサがメモリアクセスを並べ替える方法については、IntelまたはAMDのソフトウェアマニュアルを参照してください。

非常に重要なことに、ロック(およびロックを使用した同時実行セマンティクス)は通常、クロスプラットフォームの方法で実装されます...したがって、データ競合のないマルチスレッドプログラムで標準ロックを使用している場合は、クロスプラットフォームのメモリモデルについて心配する必要はありません。

興味深いことに、C ++用のMicrosoftコンパイラは、C++のメモリモデルの欠如に対処するためのC++拡張機能であるvolatileのセマンティクスを取得/リリースしていますhttp://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80).aspx。ただし、Windowsがx86 / x64でのみ実行されることを考えると、それほど多くはありません(IntelおよびAMDメモリモデルを使用すると、言語で取得/解放セマンティクスを簡単かつ効率的に実装できます)。

于 2011-07-26T04:27:04.017 に答える
31

ミューテックスを使用してすべてのデータを保護する場合は、心配する必要はありません。ミューテックスは常に十分な順序と可視性の保証を提供してきました。

ここで、アトミックまたはロックフリーアルゴリズムを使用した場合は、メモリモデルについて考える必要があります。メモリモデルは、アトミックが順序付けと可視性の保証を提供するタイミングを正確に記述し、手動でコード化された保証のためのポータブルフェンスを提供します。

以前は、アトミックはコンパイラ組み込み関数またはより高いレベルのライブラリを使用して実行されていました。フェンスは、CPU固有の命令(メモリバリア)を使用して実行されます。

于 2011-06-11T23:49:53.727 に答える
7

上記の回答は、C++メモリモデルの最も基本的な側面を示しています。実際には、std::atomic<>少なくともプログラマーが過度に最適化するまで(たとえば、あまりにも多くのことをリラックスしようとすることによって)、「ただ働く」のほとんどの使用法。

間違いがまだよくある場所が1つあります。それは、シーケンスロックです。https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdfに、課題に関する優れた読みやすいディスカッションがあります。リーダーがロックワードへの書き込みを回避するため、シーケンスロックは魅力的です。次のコードは、上記のテクニカルレポートの図1に基づいており、C++でシーケンスロックを実装する際の課題を強調しています。

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

data1最初は継ぎ目があり、直感的ではないdata2必要がありますatomic<>。それらがアトミックでない場合は、 (で)reader()書き込まれるのとまったく同時に(で)読み取ることができますwriter()。C ++メモリモデルによると、これは実際にデータを使用したことがない場合でもreader()競争です。さらに、それらがアトミックでない場合、コンパイラーは各値の最初の読み取りをレジスターにキャッシュできます。明らかに、あなたはそれを望まないでしょう...あなたはのwhileループの各反復で再読み取りしたいですreader()

atomic<>また、それらを作成してにアクセスするだけでは不十分memory_order_relaxedです。この理由は、seq(in)の読み取りには取得セマンティクスreader()しかないためです。簡単に言うと、XとYがメモリアクセスであり、XがYに先行し、Xが取得または解放ではなく、Yが取得である場合、コンパイラはXの前にYを並べ替えることができます。Yがseqの2番目の読み取りであり、Xデータの読み取りであった場合、そのような並べ替えはロックの実装を壊します。

この論文はいくつかの解決策を示しています。今日最高のパフォーマンスを発揮するのは、おそらくseqlockの2回目の読み取りのatomic_thread_fenceにwithを使用するものです。論文では、それは図6です。これまで読んだことがある人は本当に論文を読むべきなので、ここではコードを再現していません。この投稿よりも正確で完全です。memory_order_relaxed

data最後の問題は、変数をアトミックにするのは不自然かもしれないということです。コードに含めることができない場合は、非常に注意する必要があります。これは、非アトミックからアトミックへのキャストはプリミティブ型に対してのみ有効であるためです。C ++ 20はatomic_ref<>、を追加することになっています。これにより、この問題の解決が容易になります。

要約すると、C ++メモリモデルを理解していると思っていても、独自のシーケンスロックを実行する前に十分に注意する必要があります。

于 2019-12-20T03:56:25.297 に答える
-6

CおよびC++は、整形式のプログラムの実行トレースによって定義されていました。

現在、それらはプログラムの実行トレースによって半分が定義され、同期オブジェクトの多くの順序によって半分が事後的に定義されています。

これらの2つのアプローチを組み合わせる論理的な方法がないため、これらの言語定義はまったく意味がありません。特に、ミューテックスまたはアトミック変数の破棄は明確に定義されていません。

于 2019-07-28T20:09:02.437 に答える