11

並行プログラミングに関する Joe Duffy の本を読んでいます。ロックレススレッドについて学術的な質問があります。

最初に: ロックレス スレッドには危険が伴うことは知っています (信じられない場合は、メモリ モデルに関する本のセクションを読んでください)。

それにもかかわらず、質問があります。int プロパティを持つクラスがあるとします。

このプロパティによって参照される値は、複数のスレッドによって非常に頻繁に読み取られます

値が変更されることは非常にまれであり、変更される場合は単一のスレッドで変更されます。

それを使用する別の操作が進行中に変更された場合、誰も指を失うことはありません (それを使用する人が最初に行うことは、それをローカル変数にコピーすることです)。

ロックを使用できます (または、readerwriterlockslim を使用して同時読み取りを維持します)。変数 volatile をマークすることができます (これが行われる多くの例)

ただし、揮発性であっても、パフォーマンスに影響を与える可能性があります。

変更時に VolatileWrite を使用し、読み取りの場合は通常のアクセスのままにするとどうなりますか。このようなもの:

public class MyClass
{
  private int _TheProperty;
  internal int TheProperty
  {
    get { return _TheProperty; }
    set { System.Threading.Thread.VolatileWrite(ref _TheProperty, value); }
  }
}

実生活でこれを試すことはないと思いますが、答えに興味があります(何よりも、読んできたメモリモデルの内容を理解しているかどうかのチェックポイントとして)。

4

7 に答える 7

6

変数を「揮発性」としてマークすると、2つの効果があります。

1)読み取りと書き込みには取得と解放のセマンティクスがあるため、他のメモリ位置の読み取りと書き込みは、このメモリ位置の読み取りと書き込みに関して「時間的に前後に移動」しません。(これは単純化ですが、あなたは私の主張を理解します。)

2)ジッタによって生成されたコードは、論理的に変化していないように見える値を「キャッシュ」しません。

前者の点があなたのシナリオに関連しているかどうかはわかりません。1つのメモリ位置のみを記述しました。揮発性の書き込みのみがあり、揮発性の読み取りがないことが重要であるかどうかは、あなた次第です。

しかし、後者の点は非常に適切であるように私には思えます。不揮発性変数にスピンロックがある場合:

while(this.prop == 0) {}

ジッタは、あなたが書いたかのようにこのコードを生成する権利の範囲内にあります

if (this.prop == 0) { while (true) {} }

実際にそうするかどうかはわかりませんが、そうする権利があります。コードがループを一周するたびにプロパティを実際に再チェックすることが必要な場合は、揮発性としてマークするのが正しい方法です。

于 2010-01-29T00:37:15.743 に答える
4

問題は、読んでいるスレッドが変更を確認できるかどうかですすぐに見えるかどうかだけの問題ではない。

率直に言って、私はボラティリティを理解しようとすることをあきらめました - 以前は思っていたような意味ではないことはわかっています... しかし、読み取りスレッドにメモリバリアがなければ、永遠に同じ古いデータ。

于 2010-01-28T21:21:45.690 に答える
2

CPUレベルでは、はい、すべてのプロセッサが最終的にメモリアドレスの変更を確認します。ロックやメモリバリアがなくても。ロックとバリアは、プログラムに正しく見えるように、すべてが相対的な順序で発生することを保証します (他の命令を記述)。

問題はキャッシュの一貫性ではありません (Joe Duffy の本がその間違いを犯していないことを願っています)。キャッシュは一貫性を保ちます。これには時間がかかるだけであり、強制しない限り、プロセッサはそれが起こるのを待つ必要はありません。代わりに、プロセッサは次の命令に進みますが、これは前の命令の前に発生する場合と発生しない場合があります (メモリの読み取り/書き込みごとに異なる時間がかかるためです。皮肉なことに、プロセッサが同意するための時間のため)コヒーレンスなど - これにより、一部のキャッシュラインが他のキャッシュラインよりも速くコンヒーレントになります (つまり、ラインが Modified、Exclusive、Shared、または Invalid であったかどうかに応じて、必要な状態になるまでに多かれ少なかれ作業が必要になります)。

そのため、読み取りは古いか、古いキャッシュからのように見えるかもしれませんが、実際には予想よりも早く発生しただけです (通常、先読みと分岐予測のため)。実際読み取られたとき、キャッシュは一貫していましたが、それ以来変更されています。したがって、値は読んだ時点では古いものではありませんでしたが、必要なときは今です。あなたはそれを読むのが早すぎました。:-(

または同等に、コードのロジックが書かれると思っていたよりも遅く書かれました。

または両方。

とにかく、これが C/C++ の場合、ロック/バリアがなくても、最終的には更新された値を取得できます。(通常、メモリにはそれくらいの時間がかかるため、数百サイクル以内です)。C/C++ では、volatile (弱い非スレッド volatile) を使用して、値がレジスタから読み取られないようにすることができます。(現在、一貫性のないキャッシュがあります!つまり、レジスタ)

C# では、CLR について十分な知識がなく、値がレジスタに保持される期間や、メモリからの実際の再読み取りを確実に行う方法を知ることができません。「弱い」揮発性を失いました。

変数アクセスが完全にコンパイルされない限り、最終的にはレジスタが不足し (x86 には最初から多くのレジスタがありません)、再読み取りが行われるのではないかと思います。

しかし、私が見えるという保証はありません。volatile-read をコード内の特定のポイント (頻繁ではあるがそれほど頻繁ではない) に制限できる場合 (つまり、while(things_to_do) ループ内の次のタスクの開始)、それが最善の方法かもしれません。

于 2010-01-29T02:23:23.550 に答える
2

の「パフォーマンス ヒット」volatileは、コンパイラが値を最適化するのではなく、実際に値をチェックするコードを生成するためです

于 2010-01-28T21:22:57.477 に答える
1

これは、「最後のライターが勝つ」パターンが状況に当てはまる場合に私が使用するパターンです。私はこのvolatileキーワードを使用していましたが、Jeffery Richter のコード例でこのパターンを見た後、それを使い始めました。

于 2010-01-28T21:18:23.323 に答える
1

通常のもの (メモリ マップ デバイスなど) の場合、CPU/CPU 内/CPU 間で実行されるキャッシュ コヒーレンシ プロトコルは、そのメモリを共有するさまざまなスレッドが物事の一貫したビューを取得することを保証するために存在します (つまり、 1 つの CPU 内のメモリ ロケーションは、そのメモリをキャッシュに持つ他の CPU によって認識されます) この点で、volatile は、レジスタにキャッシュされた値を読み取るなどして、オプティマイザがメモリ アクセス (とにかく常にキャッシュを通過する) を最適化しないようにするのに役立ちます。C# のドキュメントは、これについてかなり明確に見えます。繰り返しますが、アプリケーション プログラマは通常、キャッシュの一貫性を自分で処理する必要はありません。

無料で入手できる論文「What Every Programmer Should Know About Memory」を読むことを強くお勧めします。多くの魔法がボンネットの下で行われており、ほとんどの場合、自分の足を撃つことができません。

于 2010-01-28T21:49:05.730 に答える
0

C# では、int型はスレッドセーフです。

書き込みを行うスレッドは 1 つだけだとおっしゃっていたので、適切な値について競合することはありません。また、ローカル コピーをキャッシュしている限り、ダーティ データを取得することはありません。

ただし、 OS スレッドが更新を行う場合は、volatileとして宣言することをお勧めします。

また、一部の操作はアトミックではなく、ライターが複数ある場合に問題が発生する可能性があることにも注意してください。たとえば、boolライターが複数ある場合に型が壊れることはありませんが、次のようなステートメントは次のようになります。

a = !a;

アトミックではありません。2 つのスレッドが同時に読み取ると、競合状態になります。

于 2010-01-28T21:18:04.640 に答える