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


1894

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

この記事Herb Sutterを引用しているGavin Clarkeによる)は、

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

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

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

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

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

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


3
@curiousguy:詳細...
Nawaz

4
@curiousguy:ブログを書いて...修正を提案してください。あなたの主張を有効かつ理論的根拠にする他の方法はありません。
Nawaz

2
私はそのサイトをQに質問し、アイデアを交換する場所と間違えました。私の悪い; それは、彼がスロースペックについてひどく彼自身と矛盾するときでさえ、あなたがハーブサッターに反対することができない適合のための場所です。
curiousguy

5
@curiousguy:C ++は標準が言うことであり、インターネット上のランダムな男が言うことではありません。したがって、はい、規格への準拠が必要です。C ++はオープンな哲学ではなく、標準に準拠していないものについて話すことができます。
Nawaz

3
「私は、C ++プログラムが明確に定義された動作を持つことができないことを証明しました。」。証明なしの背の高い主張!
Nawaz

回答:


2205

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

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

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

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

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

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

           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 037 17(スレッド1の前に実行される場合)、(スレッド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はそれらを標準化します。ただし、パフォーマンス上の理由から、低レベルのプリミティブが必要な場合があります(たとえば、従来のダブルチェックロックパターン)。新しい標準は、ミューテックスや条件変数などの高レベルのガジェットを提供し、アトミックタイプやメモリバリアのさまざまなフレーバーなどの低レベルのガジェットも提供します。したがって、標準で指定された言語内で完全に洗練された高性能のコンカレントルーチンを記述できるようになり、コードが今日のシステムと明日のシステムの両方で変更されずにコンパイルおよび実行されることを確認できます。

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

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


37
いい答えですが、これは新しいプリミティブの実際の例について本当に物乞いです。また、プリミティブなしのメモリの順序はC ++ 0x以前と同じであると思います。保証はありません。
ジョンリプリー

5
@ジョン:私は知っている、しかし私はまだ自分でプリミティブを学んでいる:-)。また、バイトアクセスがアトミック(順序付けされていません)であることを保証していると思います。そのため、この例では「char」を使用しました。しかし、そのことについては100%よくわかりません...何か良いものを提案したい場合チュートリアル」参照回答に追加します
Nemo

48
@Nawaz:はい!メモリアクセスは、コンパイラまたはCPUによって並べ替えられます。(例)キャッシュと投機的ロードについて考えます。システムメモリがヒットする順序は、コーディングした順序とは異なります。コンパイラとCPUは、このような並べ替えがシングルスレッドコードを壊さないようにします。マルチスレッドコードの場合、「メモリモデル」は、可能性のある並べ替えと、2つのスレッドが同時に同じ場所を読み書きした場合にどうなるか、および両方の制御をどのように制御するかを特徴付けます。シングルスレッドコードの場合、メモリモデルは無関係です。
Nemo

26
@ Nawaz、@ Nemo-細かい詳細:新しいメモリモデルは、などの特定の式の未定義を指定する限り、シングルスレッドコードに関連しますi = i++シーケンスポイントの古い概念は破棄されました。新しい標準では、シーケンス化されたビフォアリレーションを使用して同じことを指定していますが、これは、より一般的なスレッド間で発生するビヘイビアの概念の特別なケースです。
JohannesD

17
@ AJG85:ドラフトC ++ 0x仕様のセクション3.6.2は、「静的ストレージ期間(3.7.1)またはスレッドストレージ期間(3.7.2)を持つ変数は、他の初期化が行われる前にゼロ初期化(8.5)される必要があります。場所。" この例ではx、yはグローバルであるため、静的な保存期間があり、ゼロで初期化されると思います。
Nemo

345

私は、メモリ一貫性モデル(またはメモリモデル、略して)を理解するための類似性を示します。これは、レスリーランポートの独創的な論文「Time、Clocks、and the Ordering of Events in a Distributed System」から発想を得たものです。類推は適切であり、基本的な意味を持っていますが、多くの人々にとってやり過ぎかもしれません。ただし、メモリ整合性モデルについての推論を容易にするメンタルイメージ(画像表現)が提供されることを願っています。

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

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

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

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

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

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

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

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

相対性理論では、すべてのオブザーバーが同意する唯一の時間的順序が「時間的」イベント(つまり、原則として遅い粒子によって接続可能なイベント)間の順序のみであるため、一部順序付けられたイベントのカオスに見える順序に復元されます。真空中の光の速度よりも)。時系列の関連イベントのみが不変に順序付けられます。 Time in Physics、Craig Callender

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

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

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

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

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

キャッシュの一貫性とメモリの一貫性は混乱することがあるので、次の引用文も参考にしてください。

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

メンタルピクチャーを続けて、SWMR不変量は、任意の場所に最大で1つの粒子が存在するという物理的要件に対応しますが、任意の場所に無制限の数の観測者が存在する可能性があります。


52
特別な相対性を持つ類推のための+1、私は同じ類推を自分で作ろうとしています。プログラマーがスレッド化されたコードを調査して、特定の順序で交互にインターリーブされている異なるスレッドでの操作として動作を解釈しようとしているのを見かけることがよくあります。マルチプロセッサーシステムでは、異なる< >参照フレーム</ s>スレッドは無意味になりました。特殊相対論と比較することは、問題の複雑さを尊重させる良い方法です。
Pierre Lebeaupin 14年

71
では、宇宙はマルチコアであると結論づけるべきでしょうか?
Peter K

6
@PeterK:まさに:)そして、 これは物理学者ブライアン・グリーンによるこの時間の写真の非常に素晴らしい視覚化です:youtube.com/watch ?v=4BjGWLJNPcA&t=22m12s これは22分目の「The Illusion of Time [Full Documentary]」です12秒。
アーメドナサー

2
それは私だけですか、それとも1Dメモリモデル(横軸)から2Dメモリモデル(同時平面)に切り替えていますか?これは少し混乱しますが、それは私がネイティブスピーカーではないためかもしれません...それでも非常に興味深い読み物です。
さようならSE

重要な部分を忘れました:「ロードとストアの結果を分析することによって」...正確なタイミング情報を使用せずに。
curiousguy

115

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

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

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

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


10
その話は本当に素晴らしく、あなたがそれを見て過ごすのに3時間費やすだけの価値があります。
ZunTzu 2015

5
@ZunTzu:ほとんどのビデオプレーヤーでは、速度をオリジナルの1.25、1.5、または2倍に設定できます。
クリスチャンセヴェリン

4
@eran君たちはたまたまスライドを持ってる?チャンネル9のトークページのリンクは機能しません。
アトス

2
@athosすみません。チャネル9に連絡してみてください。削除は意図的なものではないと思います(おそらく、Herb Sutterからリンクを取得してそのまま投稿し、後でファイルを削除したと思いますが、それは単なる推測にすぎません...)。
eran 2016

75

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

POSIXスレッドまたはWindowsスレッドについて話しているとき、これは実際にはx86スレッドについて話しているように、それは少し幻想です。これは、同時に実行するハードウェア関数であるためです。C ++ 0xメモリモデルは、x86、ARM、MIPS、またはその他の思いついたものを使用しているかどうかを保証します。


28
Posixスレッドはx86に限定されません。実際、それらが最初に実装されたシステムは、おそらくx86システムではありませんでした。Posixスレッドはシステムに依存せず、すべてのPosixプラットフォームで有効です。また、Posixスレッドは協調型マルチタスクを介して実装することもできるため、ハードウェアプロパティであることも事実ではありません。しかしもちろん、ほとんどのスレッド化の問題は、ハードウェアスレッド化の実装でのみ浮上します(マルチプロセッサ/マルチコアシステムでのみ発生する場合もあります)。
celtschk 2013

57

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

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

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


2
答えが書かれたとき、Windowsはx86 / x64でのみ動作しますが、Windowsは、ある時点で、IA64、MIPS、Alpha AXP64、PowerPCおよびARMで動作します。今日では、ARMのさまざまなバージョンで実行されます。これは、x86とはメモリーの点でまったく異なり、許容範囲はほとんどありません。
LorenzoDematté16年

そのリンクはやや壊れています(「Visual Studio 2005 Retired documentation」と言います)。更新しますか?
Peter Mortensen 2017年

3
答えを書いても真実ではなかった。
Ben

同じメモリに同時にアクセスする競合する方法でアクセスする
curiousguy

27

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

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

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


19
以前の問題は、(C ++標準に関して)ミューテックスなどがないことでした。したがって、提供された唯一の保証は、ミューテックスの製造元によるものでした。これは、コードを移植しない限り問題ありませんでした(保証への小さな変更は見つけにくいため)。これで、プラットフォーム間で移植可能な標準によって提供される保証が得られます。
マーティンヨーク

4
@Martin:いずれにしても、1つはメモリモデルであり、もう1つはそのメモリモデル上で実行されるアトミックおよびスレッドプリミティブです。
ninjalj

4
また、私の指摘の大部分は、以前は言語レベルでメモリモデルがほとんどなかったということでした。それは、たまたま基盤となるCPUのメモリモデルでした。現在、コア言語の一部であるメモリモデルがあります。OTOH、ミューテックスなどは常にライブラリとして実行できます。
ninjalj

3
また、ミューテックスライブラリを作成しようとしている人にとっては、これが本当の問題になる可能性もあります。CPU、メモリコントローラー、カーネル、コンパイラ、および「Cライブラリ」がすべて別のチームによって実装されている場合、これらのいくつかは、この機能がどのように機能することになっているか、場合によっては、その機能に関して激しい意見の相違があります。私たちのシステムプログラマがアプリケーションレベルにかなりのファサードを提示するために行う必要があるのは、まったく楽しいものではありません。
zwol

11
残念ながら、言語に一貫したメモリモデルがない場合、単純なミューテックスでデータ構造を保護するだけでは不十分です。シングルスレッドコンテキストで意味のあるさまざまなコンパイラ最適化がありますが、複数のスレッドとCPUコアが関係する場合、メモリアクセスの並べ替えやその他の最適化により、未定義の動作が発生する可能性があります。詳細については、ハンス・ベームによって「スレッドがライブラリとして実装することができない」を参照してください。citeseer.ist.psu.edu/viewdoc/...
exDM69

0

上記の答えは、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(のreader())の読み取りは取得のセマンティクスしか持たないためです。簡単に言えば、XとYがメモリアクセスで、XがYに先行し、Xが取得または解放ではなく、Yが取得である場合、コンパイラはXの前にYを並べ替えることができます。Yがseqの2番目の読み取りで、Xデータの読み取りだったので、このような並べ替えはロックの実装を壊します。

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

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

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


-2

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

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

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


言語設計の改善に対するあなたの切望を共有しますが、その振る舞いが特定の言語設計原則にどのように違反するかを明確かつ明示的に示した単純なケースを中心とするならば、あなたの答えはより価値があると思います。あなたが私を許可した場合、その後、私は強く、彼らはC ++の設計によって知覚inmenseの生産性の利点の関連性に対して対比されますので、その答えにそれらの点のそれぞれの関連性のために非常に良い議論を与えるために、あなたをお勧めします
マティアスHaeussler

1
@MatiasHaeussler私の答えを誤解していると思います。私はここで特定のC ++機能の定義に異議を唱えていません(このような指摘された批判もたくさんありますが、ここにはありません)。ここでは、C ++(またはC)には明確に定義された構成体がないと主張しています。MTシーマンティクス全体は、シーケンシャルセマンティクスがなくなったため、完全に混乱しています。(私はJava MTは壊れていると思いますが、それよりは少ないと思います。)「単純な例」はほとんどすべてのMTプログラムです。同意しない場合は、MT C ++プログラムの正当性を証明する方法についての私の質問に答えてください。
curiousguy

興味深いことに、私はあなたの質問を読んだ後、あなたが何を意味しているのかをもっと理解していると思います。私が正しければ、C ++ MTプログラムの正しさの証明を開発することは不可能だとおっしゃってます。そのような場合、私にとっては、特に人工知能の到来にとって、コンピュータプログラミングの将来にとって非常に重要なことだと言えるでしょう。しかし、私が指摘しているのは、スタックオーバーフローで質問する大多数の人々にとって、彼らが気づいていないことであり、あなたが何を意味するのかを理解して興味を持った後でも
Matias Haeussler

1
「コンピュータープログラムの解体性に関する質問は、stackoverflowまたはstackexchange(どちらでもない場合はどこでも)に投稿する必要がありますか?」これはメタスタックオーバーフロー用のようですが、そうではありませんか?
マティアスヘイスラー

1
@MatiasHaeussler 1)CおよびC ++は基本的に、アトミック変数、ミューテックス、マルチスレッドの「メモリモデル」を共有します。2)これとの関連性は、「メモリモデル」を持つことの利点についてです。モデルが健全でないので、利益はゼロだと思います。
curiousguy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.