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