11
private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM];

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null)
    {
        return;
    }
    instrumentInfos[instrument.Id] = info;  // need to make it visible to other threads!
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    return instrumentInfos[instrument.Id];  // need to obtain fresh value!
}

SetInstrumentInfo and GetInstrumentInfo are called from different threads. InstrumentInfo is immutable class. Am I guaranteed to have the most recent copy when calling GetInstrumentInfo? I'm afraid that I can receive "cached" copy. Should I add kind of synchronization?

Declaring instrumentInfos as volatile wouldn't help because I need declare array items as volatile, not array itself.

Do my code has problem and if so how to fix it?

UPD1:

I need my code to work in real life not to correspond to all specifications! So if my code works in real life but will not work "theoretically" on some computer under some environment - that's ok!

  • I need my code to work on modern X64 server (currently 2 processors HP DL360p Gen8) under Windows using latest .NET Framework.
  • I don't need to work my code under strange computers or Mono or anything else
  • I don't want to introduce latency as this is HFT software. So as " Microsoft's implementation uses a strong memory model for writes. That means writes are treated as if they were volatile" I likely don't need to add extra Thread.MemoryBarrier which will do nothing but add latency. I think we can rely that Microsoft will keep using "strong memory model" in future releases. At least it's very unlikely that Microsoft will change memory model. So let's assume it will not.

UPD2:

The most recent suggestion was to use Thread.MemoryBarrier();. Now I don't understand exact places where I must insert it to make my program works on standard configuration (x64, Windows, Microsoft .NET 4.0). Remember I don't want to insert lines "just to make it possible to launch your program on IA64 or .NET 10.0". Speed is more important for me than portability. However it would be also interesting how to update my code so it will work on any computer.

UPD3

.NET 4.5 solution:

    public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
    {
        if (instrument == null || info == null)
        {
            return;
        }
        Volatile.Write(ref instrumentInfos[instrument.Id], info);
    }

    public InstrumentInfo GetInstrumentInfo(Instrument instrument)
    {
        InstrumentInfo result = Volatile.Read(ref instrumentInfos[instrument.Id]);
        return result;
    }
4

8 に答える 8

3

これは長くて複雑な答えのある質問ですが、私はそれをいくつかの実用的なアドバイスに蒸留しようとします。

1.簡単な解決策:ロックされた状態でのみinstrumentInfosにアクセスします

マルチスレッドプログラムの予測不可能性を回避する最も簡単な方法は、常にロックを使用して共有状態を保護することです。

あなたのコメントに基づくと、このソリューションは高すぎると考えているようです。その仮定を再確認することもできますが、それが実際に当てはまる場合は、残りのオプションを見てみましょう。

2.高度なソリューション:Thread.MemoryBarrier

または、Thread.MemoryBarrierを使用することもできます。

private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM]; 

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info) 
{ 
    if (instrument == null || info == null) 
    { 
        return; 
    } 

    Thread.MemoryBarrier(); // Prevents an earlier write from getting reordered with the write below

    instrumentInfos[instrument.Id] = info;  // need to make it visible to other threads! 
} 

public InstrumentInfo GetInstrumentInfo(Instrument instrument) 
{ 
    InstrumentInfo info = instrumentInfos[instrument.Id];  // need to obtain fresh value! 
    Thread.MemoryBarrier(); // Prevents a later read from getting reordered with the read above
    return info;
}

書き込み前と読み取りにThread.MemoryBarrierを使用すると、潜在的な問題を防ぐことができます。最初のメモリバリアは、書き込みスレッドがオブジェクトを配列に公開する書き込みでオブジェクトのフィールドを初期化する書き込みを並べ替えるのを防ぎ、2番目のメモリバリアは、読み取りスレッドが配列からオブジェクトを受け取る読み取りを並べ替えるのを防ぎます。そのオブジェクトのフィールドの後続の読み取り。

補足として、.NET 4は、上記のようにThread.MemoryBarrierを使用するThread.VolatileReadとThread.VolatileWriteも公開します。ただし、System.Object以外の参照型については、Thread.VolatileReadおよびThread.VolatileWriteのオーバーロードはありません。

3.高度なソリューション(.NET 4.5):Volatile.ReadおよびVolatile.Write

.NET 4.5は、完全なメモリバリアよりも効率的なVolatile.ReadメソッドとVolatile.Writeメソッドを公開します。.NET 4をターゲットにしている場合、このオプションは役に立ちません。

4.「間違っているが、たまたま機能する」ソリューション

私が言おうとしていることに決して頼ってはいけません。しかし...元のコードに存在する問題を再現できる可能性はほとんどありません。

実際、.NET 4のX64で、それを再現できたら非常に驚きます。X86-X64は、かなり強力なメモリ並べ替えの保証を提供するため、これらの種類のパブリケーションパターンはたまたま正しく機能します。.NET 4 C#コンパイラと.NET 4 CLR JITコンパイラも、パターンを壊すような最適化を回避します。したがって、メモリ操作を並べ替えることができる3つのコンポーネントのいずれもそうすることはありません。

とは言うものの、X64の.NET 4では実際には機能しないパブリケーションパターンの(ややあいまいな)バリアントがあります。したがって、コードを.NET 4 X64以外のアーキテクチャで実行する必要がないと考えている場合でも、サーバーで問題を再現できない場合でも、正しいアプローチの1つを使用すると、コードの保守性が向上します。 。

于 2012-08-15T23:57:42.873 に答える
2

これを解決するための私の好ましい方法は、クリティカルセクションを使用することです。C#には、この問題を解決する「ロック」と呼ばれる組み込みの言語構造があります。このlockステートメントは、同時にそのクリティカルセクション内に複数のスレッドが存在できないことを確認します。

private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM];

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null) {
        return;
    }
    lock (instrumentInfos) {
        instrumentInfos[instrument.Id] = info;
    }
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    lock (instrumentInfos) {
        return instrumentInfos[instrument.Id];
    }
}

これにはパフォーマンスへの影響がありますが、常に信頼できる結果が得られることが保証されます。これは、instrumentInfosオブジェクトを反復処理する場合に特に便利です。lockそのようなコードのステートメントは絶対に必要です。

lockこれは、複雑なステートメントが確実かつアトミックに実行されることを保証する汎用ソリューションであることに注意してください。あなたの場合、オブジェクトポインタを設定しているので、スレッドセーフなリストや配列のような単純な構造を使用できることがわかるかもしれません。ただし、これらがこれまでに触れた2つの関数だけである場合に限りますinstrumentInfos。詳細については、こちらをご覧ください:C#アレイはスレッドセーフですか?

編集:あなたのコードについての重要な質問は:何InstrumentInfoですか?から派生した場合は、別のスレッドでオブジェクトを更新すると問題が発生する可能性があるため、共有配列を更新するたびobjectに常に新しいオブジェクトを作成するように注意する必要があります。InstrumentInfoの場合、ステートメント内の値は書き込みと読み取りでコピーされるためstructlockステートメントは必要なすべてのセキュリティを提供します。

編集2:もう少し調査すると、特定のコンパイラ構成でスレッド共有変数への変更が表示されない場合があるようです。このスレッドでは、「ブール」値を一方のスレッドに設定でき、もう一方のスレッドでは取得できない場合について説明します。C#スレッドは実際に値をキャッシュし、他のスレッドでその値への変更を無視できますか?

ただし、このケースは.NET値型が関係する場合にのみ存在することに気付きました。ジョー・エリクソンの反応を見れば。この特定のコードの逆コンパイルは、理由を示しています。スレッド共有値がレジスターに読み込まれ、コンパイラーがループ内の不要な読み取りを最適化するため、再読み取りは行われません。

共有オブジェクトの配列を使用していて、すべてのアクセサーをカプセル化していることを考えると、rawメソッドを使用しても完全に安全だと思います。ただし、ロックを使用することによるパフォーマンスの低下は非常に小さいため、事実上忘れられます。たとえば、1,000万回の呼び出しを試みた簡単なテストアプリケーションを作成しSetInstrumentInfoましGetInstrumentInfoた。パフォーマンスの結果は次のとおりです。

  • ロックなしで、1,000万件の設定と通話の受信:2秒149ミリ秒
  • ロックあり、1,000万回の設定と通話の受信:2秒195ミリ秒

これは、おおよそ次の生のパフォーマンスに変換されます。

  • ロックせずに、1秒あたり4653327のgetおよびset呼び出しを実行できます。
  • ロックを使用すると、1秒あたり4555808のgetおよびset呼び出しを実行できます。

私の見解では、このlock()ステートメントは2.1%のパフォーマンスのトレードオフの価値があります。私の経験では、getこのsetようなメソッドは、アプリケーションの実行時間のごく一部しか占めていません。他の場所で潜在的な最適化を探すか、より高いクロックレートのCPUを取得するために追加のお金を費やすのがおそらく最善でしょう。

于 2012-08-14T18:29:07.570 に答える
2

システムレベルの同期プリミティブ(Mutexなど)を使用できます。しかし、それらは少し手間がかかります。Mutexはカーネルレベルのプリミティブであるため、非常に低速です。これは、プロセス間で相互に排他的なコードを持つことができることを意味します。これは必要ではなく、単一のプロセス内で機能する、またはlockのようなパフォーマンスのコストがはるかに低いものを使用できます。Monitor.EnterMonitor.Exit

配列内の要素がアトミックに割り当てられていない場合は、コレクションに書き込む動作をアトミックにする必要があるため、 /VolatileReadのようなものを使用することはできません。/またはそれをしません。これらは、セマンティクスを取得して解放するだけです。これは、書き込まれたものがすべて他のスレッドに「表示」されることを意味します。コレクションへの書き込みをアトミックにしないと、データが破損する可能性があります。取得/解放のセマンティクスはそれを助けません。WriteMemoryBarrierVolatileReadWriteMemoryBarrier

例えば:

private InstrumentInfo[] instrumentInfos = new InstrumentInfo[Constants.MAX_INSTRUMENTS_NUMBER_IN_SYSTEM];

private readonly object locker = new object();

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null)
    {
        return;
    }
    lock(locker)
    {
        instrumentInfos[instrument.Id] = info;
    }
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    lock(locker)
    {
        return instrumentInfos[instrument.Id];
    }
}

並行コレクションは同じことを行います。ただし、すべての操作が同期によって保護されているため、コストが発生します。さらに、並行コレクションは要素へのアクセスの原子性についてのみ認識しますが、アプリケーションレベルで原子性を確認する必要があります。並行コレクションで次のことを行った場合:

public bool Contains(Instrument instrument)
{
   foreach(var element in instrumentInfos)
   {
       if(element == instrument) return true;
   }
}

...少なくともいくつかの問題があります。1つは、列挙中にSetInstrumentInfoがコレクションを変更するのを止めていないことです(多くのコレクションはこれをサポートしておらず、例外をスローします)。つまり、コレクションは単一の要素の取得でのみ「保護」されます。2つ目は、コレクションは各反復で保護されます。100個の要素があり、並行コレクションがロックを使用している場合、100個のロックを取得します(検索する要素が最後であるか、まったく見つからないと仮定します)。これは、必要以上に遅くなります。並行コレクションを使用しない場合は、単にロックを使用して同じ結果を得ることができます。

public bool Contains(Instrument instrument)
{
   lock(locker)
   {
      foreach(var element in instrumentInfos)
      {
          if(element == instrument) return true;
      }
   }
}

単一のロックがあり、パフォーマンスが大幅に向上します。

更新構造体の場合、(または他の同期プリミティブ)の使用がさらに重要になること を指摘することが重要だと思います。これInstrumentInfoは、値のセマンティクスがあり、すべての割り当てで、フィールドを格納するために使用するのと同じ数のバイトを移動する必要があるためです(つまり、 32ビットまたは64ビットのネイティブ単語参照割り当てではなくなりました)。つまり、単純な割り当て(たとえば、配列要素への)はアトミックではないため、スレッドセーフではありません。lockInstrumentInfoInstrumentInfo

UPDATE 2InstrumentInfo配列要素を置き換える操作が潜在的にアトミックである場合(が参照であり、IL命令がアトミックである場合、単に参照割り当てになりますstelem.ref)。(つまり、要素を配列に書き込むという行為は、私が上で述べたものとはアトミックinstrumentInfosです)この場合、処理するコードが投稿したものだけである場合は、次を使用できますThread.MemoryBarrier()

public void SetInstrumentInfo(Instrument instrument, InstrumentInfo info)
{
    if (instrument == null || info == null)
    {
        return;
    }
    Thread.MemoryBarrier();
    instrumentInfos[instrument.Id] = info;
}

public InstrumentInfo GetInstrumentInfo(Instrument instrument)
{
    var result = instrumentInfos[instrument.Id];
    Thread.MemoryBarrier();
    return result;
}

volatile...可能であれば、配列内の各要素を宣言するのと同じです。

VolatileWrite書き込む変数への参照が必要なため、機能しません。配列要素への参照を指定することはできません。

于 2012-08-14T19:01:31.973 に答える
0

私が長年のデバイスドライバープログラミングから知っている限り、volatileは、CPUの制御外で、つまりハードウェアの介入によって変更される可能性のあるもの、またはシステム空間にマップされたハードウェアメモリ(CSRなど)に使用されます。スレッドがメモリ位置を更新すると、CPUはキャッシュラインをロックし、プロセッサ間割り込みを発生させて、他のCPUがそれを破棄できるようにします。したがって、古いデータを読み取る可能性はありません。理論的には、データがアトミック(複数のクワッド)でない場合、リーダーが部分的に更新されたデータを読み取る可能性があるため、同じ配列位置への同時書き込みと読み取りについてのみ心配する可能性があります。これは、参照の配列では発生しないと思います。

携帯電話のカメラテスターボードからのストリーミングビデオを表示するために、過去に開発したアプリとドライバーに似ているため、あなたが達成しようとしていることを大胆に推測します。ビデオディスプレイアプリは、各フレームに基本的な画像操作(ホワイトバランス、オフセットピクセルなど)を適用できます。これらの設定は、UIから設定され、処理スレッドからGETされました。処理スレッドの観点からは、設定は不変でした。ビデオではなく、ある種のオーディオで同様のことをしようとしているようです。

私がC++アプリで採用したアプローチは、現在の設定を「フレーム」に付随する構造体にコピーすることでした。したがって、各フレームには、それに適用される設定の独自のコピーがありました。UIスレッドは設定への変更を書き込むためにロックを実行し、処理スレッドは設定をコピーするためにロックを実行しました。複数のクワッドが移動したため、ロックが必要でした。ロックがないと、部分的に更新された設定を確実に読み取ることができます。ストリーミング中にフレームがめちゃくちゃになっていることに気付く人はおそらくいないので、それほど重要ではありませんが、ビデオを一時停止したり、フレームをディスクに保存したりすると、暗い壁の中に光沢のある緑色のピクセルが見つかるはずです。音の場合、蒸している間でもグリッチを見つけるのははるかに簡単です。

それがケース1でした。ケース2を見てみましょう。

デバイスが不明な数のスレッドによって使用されているときに、デバイスを根本的に再構成するにはどうすればよいですか?先に進んでそれを実行すると、いくつかのスレッドが構成Aで開始され、その過程で構成Bに直面することが保証されます。これは、高い確実性で死を意味します。ここで、リーダーライターロックなどが必要になります。つまり、新しいアクティビティをブロックしながら、現在のアクティビティが終了するのを待つことができる同期プリミティブです。それがリーダー・ライター・ロックの本質です。

ケース2の終わり。もちろん、私の大げさな推測が正しければ、あなたの問題は何かを見てみましょう。

ワーカースレッドが処理サイクルの開始時にGETを実行し、サイクル全体でその参照を保持する場合、参照の更新はPeterが述べたようにアトミックであるため、ロックは必要ありません。これは私が提案する実装​​であり、各フレームの処理の開始時に設定構造をコピーするのと同じです。

ただし、コード全体から複数のGETを実行している場合は、問題が発生します。同じ処理サイクル内で、一部の呼び出しが参照Aを返し、一部の呼び出しが参照Bを返すためです。アプリで問題がない場合は、幸運な男。

ただし、この問題に問題がある場合は、バグを修正するか、その上にビルドしてパッチを適用する必要があります。バグの修正は簡単です。参照を渡すことができるように少し書き直す必要がある場合でも、複数のGETを削除します。

バグにパッチを適用する場合は、リーダーライターロックを使用してください。各スレッドはリーダーロックを取得し、GETを実行してから、サイクルが終了するまでロックを保持します。次に、リーダーロックを解除します。配列を更新するスレッドは、ライターロックを取得し、SETを実行して、ライターロックをすぐに解放します。

このソリューションはほぼ機能しますが、リーダーロックであっても、コードが長期間ロックを保持していると悪臭を放ちます。

ただし、マルチスレッドプログラミングを適切に忘れたとしても、さらに微妙な問題があります。スレッドは、サイクルの開始時に読み取りロックを取得し、終了時にそれを解放してから、新しいサイクルを開始して再取得します。ライターが表示されると、スレッドはライターが完了するまで新しいサイクルを開始できません。つまり、現在機能している他のすべてのリーダースレッドの処理が完了するまでです。つまり、一部のスレッドは、サイクルを再開する前に予期しない大きな遅延が発生します。これは、優先度の高いスレッドであっても、現在のすべてのリーダーもサイクルを終了するまで、ライターロックの要求によってブロックされるためです。オーディオやタイムクリティカルな何かをしている場合は、そのような動作を避けたいと思うかもしれません。

私は良い推測をし、私の説明が明確であることを願っています:-)

于 2012-08-14T19:41:01.493 に答える
0

なんらかの理由で知る時間がないので、どこにもコメントを追加できなかったので、別の回答を追加しました。ご迷惑をおかけし申し訳ございません。

UPD3はもちろん機能しますが、危険なほど誤解を招く恐れがあります。

UPD3ソリューションが実際に必要な場合は、次のシナリオを考えてください。ThreadAはCPU1で実行され、Var1へのアトミック更新を実行します。次に、ThreadAがプリエンプションされ、スケジューラーはCPU2でそれを再スケジュールすることを決定します。おっと...Var1はCPU1のキャッシュにあるので、UPD3の完全に誤ったロジックによれば、ThreadAはVar1に揮発性の書き込みを行ってから、揮発性の読み取りを行う必要があります。Oooopsss ...スレッドは、いつ再スケジュールされるか、またはどのCPUで終了するかを知りません。したがって、UPD3の完全に誤ったロジックによれば、スレッドは常に揮発性の書き込みと揮発性の読み取りを実行する必要があります。そうしないと、古いデータを読み取る可能性があります。つまり、すべてのスレッドが常に揮発性の読み取り/書き込みを実行する必要があります。

シングルスレッドの場合、マルチバイト構造を更新するとき(非アトミック更新)、問題はさらに悪化します。一部の更新がCPU1、一部がCPU2、一部がCPU3などで発生したと想像してください。

なぜ世界はまだ終わっていないのか、ちょっと不思議に思います。

于 2012-08-21T12:38:05.627 に答える
-1

または、クラスなど、System.Collections.Concurrent名前空間の一部のクラスを使用することもできますConcurrentBag<T>。これらのクラスは、スレッドセーフになるように構築されています。

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

于 2012-08-14T18:35:51.130 に答える
-1

この場合、 ConcurrentDictionaryを活用できます。スレッドセーフです。

于 2012-08-14T18:35:57.813 に答える
-1

非常に興味深い質問です。エリックかジョンが記録を更新するために来てくれることを願っていますが、それまでは最善を尽くします。また、この答えが正しいかどうか100%確信が持てないということで、これに接頭辞を付けさせてください。

通常、「このフィールドを揮発性にしますか?」を扱う場合。質問、あなたはこのような状況について心配しています:

SomeClass foo = new SomeClass("foo");

void Thread1Method() {
    foo = new SomeClass("Bar");
    ....
}

void Thread2Method() {
    if (foo.Name == "foo") ...
    ...
    // Here, the old value of foo might be cached 
    // even if Thread1Method has already updated it.
    // Making foo volatile will fix that.
    if (foo.Name == "bar") ...
}

あなたの場合、配列全体のアドレスを変更するのではなく、その配列内の要素の値を変更します。これは、(おそらく)ネイティブワードサイズのマネージポインターです。これらの変更はアトミックですが、メモリバリア(C#仕様による取得/解放セマンティクス)があり、アレイ自体をフラッシュすることが保証されているかどうかはわかりません。volatileしたがって、配列自体にを追加することをお勧めします。

配列自体が揮発性である場合、読み取りスレッドが配列からアイテムをキャッシュすることは不可能だと思います。配列内の要素をインプレースで変更していないため、要素を揮発性にする必要はないようです。要素を新しい要素に完全に置き換えています(少なくとも見た目は!)。

あなたがトラブルに巻き込まれるかもしれないところはこれです:

var myTrumpet = new Instrument { Id = 5 };
var trumpetInfo = GetInstrumentInfo(myTrumpet);
trumpetInfo.Name = "Trumpet";
SetInstrumentInfo(myTrumpet, trumpetInfo);

ここでは、アイテムを所定の位置で更新しているので、キャッシュが発生するのを見てそれほど驚かないでしょう。しかし、それが本当にそうなるかどうかはわかりません。MSILには、配列要素の読み取りと書き込み用の明示的なオペコードが含まれているため、セマンティクスは標準のヒープの読み取りと書き込みとは異なる場合があります。

于 2012-08-14T19:15:17.533 に答える