18

以前、私はいくつかの非常に単純なマルチスレッドコードを作成しましたが、実行中の途中でコンテキストスイッチが発生する可能性があることを常に認識していたため、共有変数へのアクセスを常に保護してきました。 CCriticalSectionクラスは、構築のクリティカルセクションに入り、破棄されます。私はこれがかなり攻撃的であることを知っており、クリティカルセクションに頻繁に、時にはひどく出入りします(たとえば、CCriticalSectionをよりタイトなコードブロック内に置くことができる関数の開始時)が、コードはクラッシュせず、十分に高速に実行されます。

仕事で私のマルチスレッドコードはよりタイトである必要があり、必要な最低レベルでのロック/同期のみです。

仕事で私はいくつかのマルチスレッドコードをデバッグしようとしていました、そして私はこれに出くわしました:

EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);

さて、m_bSomeVariableWin32 BOOL(揮発性ではない)です。これは、私が知る限り、intとして定義されており、x86では、これらの値の読み取りと書き込みは単一の命令であり、コンテキストスイッチは命令境界で発生するため、必要はありません。この操作をクリティカルセクションと同期させるため。

この操作に同期が必要かどうかを確認するためにオンラインでさらに調査を行い、次の2つのシナリオを考え出しました。

  1. CPUがアウトオブオーダー実行を実装しているか、2番目のスレッドが別のコアで実行されており、更新された値が他のコアが確認できるようにRAMに書き込まれていません。と
  2. intは4バイトに揃えられていません。

ナンバー1は「volatile」キーワードで解決できると思います。VS2005以降では、C ++コンパイラはメモリバリアを使用してこの変数へのアクセスを囲み、使用する前に変数が常にメインシステムメモリに完全に書き込まれる/読み取られるようにします。

番号2確認できません。バイトアラインメントがなぜ違いを生むのかわかりません。x86命令セットはわかりませんがmov、4バイトにアラインされたアドレスを指定する必要がありますか?そうでない場合は、指示を組み合わせて使用​​する必要がありますか?それは問題を引き起こすでしょう。

それで...

質問1:「volatile」キーワード(メモリバリアを使用し、このコードを最適化しないようにコンパイラにヒントを与えることを暗示する)を使用すると、プログラマはx86/x64変数の4バイト/8バイトを読み取り/間で同期する必要がなくなります。書き込み操作?

質問2:変数が4バイト/ 8バイトに整列されているという明示的な要件はありますか?

コードとクラスで定義された変数をさらに掘り下げました。

class CExample
{

private:

    CRITICAL_SECTION m_Crit1; // Protects variable a
    CRITICAL_SECTION m_Crit2; // Protects variable b
    CRITICAL_SECTION m_Crit3; // Protects variable c
    CRITICAL_SECTION m_Crit4; // Protects variable d

    // ...

};

さて、私にはこれは過度に思えます。クリティカルセクションはプロセス間でスレッドを同期すると思ったので、ある場合はそのセクションに入ることができ、そのプロセス内の他のスレッドは実行できません。保護したい変数ごとにクリティカルセクションは必要ありません。クリティカルセクションにいる場合は、他に何も邪魔することはできません。

クリティカルセクションの外部から変数を変更できる唯一のことは、プロセスが別のプロセスとメモリページを共有し(それを実行できますか?)、他のプロセスが値を変更し始める場合だと思います。ミューテックスもここで役立ちます。名前付きミューテックスはプロセス間で共有されますか、それとも同じ名前のプロセスのみですか?

質問3:クリティカルセクションの分析は正しいですか?このコードはミューテックスを使用するように書き直す必要がありますか?他の同期オブジェクト(セマフォとスピンロック)を見てきましたが、ここの方が適していますか?

質問4:クリティカルセクション/ミューテックス/セマフォ/スピンロックはどこに最適ですか?つまり、どの同期問題に適用する必要があるかです。どちらかを選択すると、パフォーマンスが大幅に低下しますか?

そして、私たちがそれに取り組んでいる間、私は、スピンロックはシングルコアマルチスレッド環境では使用されるべきではなく、マルチコアマルチスレッド環境でのみ使用されるべきであることを読みました。それで、質問5:これは間違っていますか、そうでない場合は、なぜ正しいのですか?

返信ありがとうございます:)

4

6 に答える 6

13

1) 揮発性は、まだ半分更新される可能性があるたびに、メモリから値を再ロードすると言っているだけです。

編集:2)Windowsはいくつかのアトミック関数を提供します。「連動」機能を参照してください。

コメントのおかげで、もう少し読み進めることができました。Intel System Programming Guideを読むと、読み取りと書き込みがアトミックであることがわかります。

8.1.1保証されたアトミック操作 Intel486 プロセッサ (およびそれ以降の新しいプロセッサ) では、次の基本的なメモリ操作が常にアトミックに実行されることが保証されてい
ます 。 Pentium プロセッサ (およびそれ以降の新しいプロセッサ) では、次の追加のメモリ操作が常にアトミック に実行されることが保証されてい ます 。 32 ビット データ バス内に収まるキャッシュされていないメモリ ロケーションへのビット アクセス





P6 ファミリ プロセッサ (およびそれ以降の新しいプロセッサ) では、次の追加のメモリ操作が常にアトミックに実行されることが保証されています

バス幅、キャッシュ ライン、およびページ境界に分割されたキャッシュ可能なメモリへのアクセスは、Intel Core 2 Duo、Intel Atom、Intel Core Duo、Pentium M、Pentium 4、Intel Xeon、P6 ファミリ、Pentium によってアトミックであることが保証されていません。 、および Intel486 プロセッサ。Intel Core 2 Duo、Intel Atom、Intel Core Duo、Pentium M、Pentium 4、Intel Xeon、および P6 ファミリ プロセッサは、外部メモリ サブシステムが分割アクセスをアトミックにできるようにするバス制御信号を提供します。ただし、アライメントされていないデータ アクセスはプロセッサのパフォーマンスに深刻な影響を与えるため、避ける必要があります。クワッドワードより大きいデータにアクセスする x87 命令または SSE 命令は、複数のメモリ アクセスを使用して実装できます。そのような命令がメモリに格納される場合、一部のアクセスは完了する (メモリへの書き込み) 可能性がありますが、別のアクセスはアーキテクチャ上の理由 (たとえば、「存在しない」とマークされたページ テーブル エントリが原因) で操作が失敗する可能性があります。この場合、命令全体がフォルトを引き起こしたとしても、完了したアクセスの影響がソフトウェアに表示される場合があります。TLB の無効化が遅れている場合 (セクション 4.10.3.4 を参照)、すべてのアクセスが同じページに対してであっても、このようなページ フォールトが発生する可能性があります。

したがって、基本的にはい、任意のアドレスから8ビットの読み取り/書き込みを行う場合、16ビットで整列されたアドレスなどから16ビットの読み取り/書き込みを行う場合、アトミック操作が行われます。また、最新のマシンのキャッシュライン内でアライメントされていないメモリの読み取り/書き込みを実行できることにも注目してください。ルールは非常に複雑に見えるので、私があなただったら頼りません。コメンターに乾杯、それは私にとって良い学習体験です:)

3) クリティカル セクションは、そのロックのためにスピン ロックを数回試行し、ミューテックスをロックします。スピンロックは何もせずに CPU パワーを消費する可能性があり、mutex は処理に時間がかかる可能性があります。連動機能を使用できない場合は、CriticalSections が適しています。

4) どちらかを選択すると、パフォーマンスが低下します。ここですべての利点を体験することは、かなり大きな要求です。MSDN ヘルプには、これらのそれぞれに関する有益な情報がたくさんあります。それらを読むことをお勧めします。

5)シングルスレッド環境でスピンロックを使用できますが、スレッド管理は2つのプロセッサが同じデータに同時にアクセスできないことを意味するため、通常は必要ありません。それは不可能です。

于 2010-03-31T10:58:36.197 に答える
8

1: 揮発性自体は、マルチスレッドにはほとんど役に立ちません。値をレジスタに格納するのではなく、読み取り/書き込みが実行されることを保証し、読み取り/書き込みが他の読み取り/書き込みに対してvolatile並べ替えられないことを保証します。ただし、基本的にコードの99.9%である不揮発性のものに関しては、まだ並べ替えられる場合があります。Microsoft はvolatile、すべてのアクセスをメモリ バリアでラップするように再定義しましたが、一般的にそうであるとは限りません。volatile標準で定義されているように定義されているコンパイラでは、黙って壊れます。(コードはコンパイルおよび実行されますが、スレッドセーフではなくなります)

それとは別に、整数サイズのオブジェクトへの読み取り/書き込みは、オブジェクトが適切に配置されている限り、x86 ではアトミックです。(ただし、書き込みがいつ発生するかについての保証はありません。コンパイラとCPUはそれを並べ替える可能性があるため、アトミックですが、スレッドセーフではありません)

2: はい、読み取り/書き込みをアトミックにするには、オブジェクトを位置合わせする必要があります。

3: そうでもない。一度に特定のクリティカル セクション内のコードを実行できるスレッドは 1 つだけです。他のスレッドは引き続き他のコードを実行できます。したがって、4 つの変数をそれぞれ異なるクリティカル セクションで保護することができます。それらがすべて同じクリティカル セクションを共有している場合、オブジェクト 2 を操作しているときにオブジェクト 1 を操作することはできません。これは非効率的であり、必要以上に並列処理を制限します。それらが異なるクリティカル セクションによって保護されている場合、両方が同じオブジェクトを同時に操作することはできません。

4: スピンロックが良い考えになることはめったにありません。スレッドがロックを取得できるようになるまで非常に短い時間だけ待機する必要があり、レイテンシを最小限に抑える必要がある場合に役立ちます。比較的遅い操作である OS コンテキストの切り替えを回避します。代わりに、スレッドは常に変数をポーリングするループに留まります。そのため、CPU 使用率が高くなります (スピンロックを待機している間、コアは別のスレッドを実行するために解放されません) が、ロックが解放されるとすぐにスレッドは続行できます。

他のものについては、パフォーマンス特性はほとんど同じです。ニーズに最も適したセマンティクスを持つ方を使用してください。通常、クリティカル セクションは共有変数を保護するのに最も便利であり、mutex を使用して「フラグ」を設定し、他のスレッドが続行できるようにすることが簡単にできます。

シングルコア環境でスピンロックを使用しないことに関しては、スピンロックは実際には解放されないことに注意してください。スピンロックを待機しているスレッド A は実際には保留にされず、OS はスレッド B の実行をスケジュールできます。しかし、A はこのスピンロックを待機しているため、他のスレッドがそのロックを解放する必要があります。コアが 1 つしかない場合、他のスレッドは A が切り替えられたときにのみ実行できます。正常な OS では、遅かれ早かれ、通常のコンテキスト切り替えの一部として、いずれにせよ発生するでしょう。しかし、B がロックを実行して解放するまで、A はロックを取得できないことがわかっているため、A がすぐに解放され、OS によって待機キューに入れられた方がよいでしょう。 B がロックを解除したときに再起動します。そして、それは他のすべてのものですロックタイプはそうです。スピンロックはシングル コア環境でも機能しますが (OS がプリエンプティブ マルチタスク機能を備えていると仮定して)、非常に非効率的です。

于 2010-03-31T12:25:55.337 に答える
7

Q1: 「volatile」キーワードの使用

VS2005 以降では、C++ コンパイラはメモリ バリアを使用してこの変数へのアクセスを囲み、変数を使用する前に常にメイン システム メモリに完全に書き込み/読み取りが行われるようにします。

丁度。移植可能なコードを作成していない場合、Visual Studio はまさにこの方法で実装します。ポータブルにしたい場合、オプションは現在「制限されています」。C++0x までは、読み取り/書き込み順序が保証されたアトミック操作を指定する移植可能な方法がなく、プラットフォームごとのソリューションを実装する必要があります。とはいえ、boost はすでに汚い仕事をしてくれました。そのアトミック プリミティブを使用できます。

Q2: 変数は 4 バイト/8 バイトにアラインする必要がありますか?

それらを一直線に並べておけば、安全です。そうしないと、ルールが複雑になります (キャッシュ ラインなど)。したがって、最も安全な方法は、それらを整列させておくことです。これは簡単に実現できます。

Q3: このコードはミューテックスを使用するように書き直す必要がありますか?

クリティカル セクションは、軽量ミューテックスです。プロセス間で同期する必要がない限り、クリティカル セクションを使用してください。

Q4: クリティカル セクション/ミューテックス/セマフォ/スピンロックはどこに最適ですか?

クリティカル セクションは、スピン待機を行うことさえできます。

Q5: シングルコアではスピンロックを使用しないでください

スピン ロックは、待機中の CPU がスピンしている間に、別の CPU がロックを解放する可能性があるという事実を利用します。これは 1 つの CPU だけでは発生しないため、時間の無駄です。マルチ CPU でのスピン ロックは良い考えですが、スピン待機が成功する頻度によって異なります。アイデアは、しばらく待機することは、コンテキスト スイッチを行ったり戻したりするよりもはるかに高速であるため、待機が短い可能性がある場合は、待機することをお勧めします。

于 2010-03-31T12:49:13.157 に答える
5

volatileは使用しないでください。スレッドセーフとは実質的に何の関係もありません。ローダウンについては、こちらをご覧ください。

BOOLへの割り当てには、同期プリミティブは必要ありません。特別な努力をしなくても問題なく動作します。

変数を設定してから、別のスレッドが新しい値を認識できるようにする場合は、2つのスレッド間に何らかの通信を確立する必要があります。ロックを取得する前に他のスレッドが行ったり来たりした可能性があるため、割り当てる直前にロックするだけでは何も達成されません。

最後に注意すべき点は、スレッド化を正しく行うのは非常に難しいということです。最も経験豊富なプログラマーは、スレッドの使用に最も慣れていない傾向があります。スレッドの使用に不慣れな人には、アラームベルが鳴るように設定する必要があります。アプリに同時実行性を実装するには、いくつかの高レベルのプリミティブを使用することを強くお勧めします。同期されたキューを介して不変のデータ構造を渡すことは、危険を大幅に減らす1つのアプローチです。

于 2010-03-31T10:53:09.607 に答える
3

揮発性はメモリバリアを意味しません。

それは、それが記憶モデルの知覚状態の一部になることを意味するだけです。これは、コンパイラが変数を最適化して取り除くことも、CPU レジスタでのみ変数に対して操作を実行することもできないことを意味します (実際には、メモリにロードして格納します)。

メモリバリアが暗示されていないため、コンパイラは命令を自由に並べ替えることができます。唯一の保証は、さまざまな volatile 変数が読み取り/書き込みされる順序がコードと同じになることです。

void test() 
{
    volatile int a;
    volatile int b;
    int c;

    c = 1;
    a = 5;
    b = 3;
}

c上記のコードでは (が最適化されていないと仮定して) への更新がおよびcへの更新の前または後に発生する可能性があり、3 つの結果が得られます。との更新は、順番に実行されることが保証されています。どのコンパイラでも簡単に最適化できます。十分な情報があれば、コンパイラは最適化して取り除くこともできます (他のスレッドが変数を読み取らず、変数がハードウェア配列にバインドされていないことが証明できる場合 (したがって、この場合、それらは実際に削除できます))。標準は特定の動作を要求するのではなく、ルールで認識可能な状態を要求します。ababcabas-if

于 2010-03-31T11:07:07.587 に答える
2

質問 3: CRITICAL_SECTION と Mutex はほとんど同じように機能します。Win32 ミューテックスはカーネル オブジェクトであるため、プロセス間で共有でき、CRITICAL_SECTION では実行できない WaitForMultipleObjects で待機できます。一方、CRITICAL_SECTION は軽量であるため、高速です。ただし、コードのロジックは、使用するものに影響されないようにする必要があります。

また、「保護したい変数ごとにクリティカル セクションは必要ありません。クリティカル セクションにいる場合は、他に邪魔されることはありません」ともコメントしています。これは本当ですが、トレードオフは、変数のいずれかにアクセスするには、そのロックを保持する必要があることです。変数を独立して有意に更新できる場合、それらの操作を並列化する機会を失っています。(ただし、これらは同じオブジェクトのメンバーであるため、実際に互いに独立してアクセスできると結論付ける前に、よく考えてください。)

于 2010-03-31T12:28:47.093 に答える