まず、あなたは言語弁護士のように考えることを学ぶ必要があります。
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 0
C ++ 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はそれらを標準化します。ただし、パフォーマンス上の理由から、低レベルのプリミティブ(たとえば、従来のダブルチェックロックパターン)が必要な場合があります。新しい標準は、ミューテックスや条件変数などの高レベルのガジェットを提供し、アトミックタイプやさまざまなフレーバーのメモリバリアなどの低レベルのガジェットも提供します。これで、完全に標準で指定された言語内で洗練された高性能の並行ルーチンを記述でき、コードが現在のシステムと将来のシステムの両方で変更されずにコンパイルおよび実行されることを確認できます。
率直に言って、あなたが専門家であり、深刻な低レベルのコードに取り組んでいない限り、おそらくミューテックスと条件変数に固執する必要があります。それが私がやろうとしていることです。
この内容の詳細については、このブログ投稿を参照してください。