forループで配列を反復処理することは、C#のスレッドセーフ操作ですか?IEnumerable <T>をforeachループで反復するのはどうですか?


8

私の理解に基づいて、C#の配列を指定して、複数のスレッドから同時に配列を反復処理の行為は、あるスレッド安全な操作。

することにより、配列を反復処理 Iは、によって、アレイ内のすべての位置を読んで意味昔ながらのforループ。各スレッドは単に配列内のメモリ位置のコンテンツを読み取っているだけで、誰も何も書き込んでいないため、すべてのスレッドが一貫した方法で同じものを読み取ります。

これは、上で書いたことを実行するコードの一部です。

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      for (int i = 0; i < Names.Length; i++) 
      {
        temp.Add(Names[i].Length * 2);
      }

      return temp;
   }
}

したがって、私の理解では、メソッドDoSomethingUseless はスレッドセーフありstring[]、をスレッドセーフタイプ(ImmutableArray<string>たとえば)に置き換える必要はありません

私は正しいですか?

次に、のインスタンスがあるとしますIEnumerable<T>。基本となるオブジェクトが何であるかはわかりません。オブジェクトを実装していることがわかっているだけなIEnumerable<T>ので、foreachループを使用してオブジェクトを反復処理できます。

私の理解に基づいて、このシナリオでは、複数のスレッドからこのオブジェクトを同時に反復することがスレッドセーフな操作であるという保証はありません。別の言い方をするとIEnumerable<T>、異なるスレッドからインスタンスを同時に反復すると、オブジェクトの内部状態が破壊され、オブジェクトが破損する可能性があります。

私はこの点で正しいですか?

クラスのIEnumerable<T>実装はどうArrayですか?スレッドセーフですか?

言い換えれば、次のコードはスレッドセーフですか?(これは上記とまったく同じコードですが、foreachループの代わりにループを使用して配列が反復されforます)

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      foreach (var name in Names) 
      {
        temp.Add(name.Length * 2);
      }

      return temp;
   }
}

IEnumerable<T>.NET基本クラスライブラリのどの実装が実際にスレッドセーフであるかを示すリファレンスはありますか?


4
@Christopher:再:あなたの最初のコメント:配列が中であるエンリコのシナリオでは、読み取り専用、および配列参照の保存や配列の要素のいずれもが変異している、複数の読者のシナリオは、追加の障壁なしに安全でなければなりません。
Eric Lippert

3
@Christopher 「いいえ、配列を破棄することはスレッドの保存ではありません。」- Arrayすべてのスレッドが配列を読み取るだけである限り、配列(つまり)の反復処理はスレッドセーフになります。と同じでList<T>、おそらく他のすべてのMicrosoftが作成したコレクションです。しかしIEnumerable、列挙子でスレッドセーフではないことを行う独自のものを作成することは可能です。したがって、実装IEnumerableするすべてがスレッドセーフであるという保証はありません。
Gabriel Luci

6
@Christopher:Re:2番目のコメント:いいえ、foreach列挙子だけでは機能しません。コンパイラーは、コレクションが配列であることを示す静的な型情報がある場合、列挙子の生成をスキップし、forループのように配列に直接アクセスするだけであることを知るのに十分スマートです。同様に、コレクションがのカスタム実装をサポートしている場合は、GetEnumeratorを呼び出す代わりにその実装が使用されIEnumerable.GetEnumeratorます。foreach入稿のみを行うという考え方IEnumerable/IEnumeratorは誤りです。
Eric Lippert

3
@クリストファー:Re:3番目のコメント:注意してください。「古い」というようなものもあるかもしれませんが、「新しい」というようなものもあるという考え方は正確ではありません。C#ではさまざまなスレッドが読み取りと書き込みのさまざまな順序を監視できるため、「最新」という概念はグローバルに定義されていないvolatileため、変数に対する最新の更新が得られるとは限りません。鮮度について推論する代わりに、書き込みの並べ替え方法にどのような制限が課さ volatileれるかについて推論します。
Eric Lippert

6
@EnricoMassone:私はあなたの質問のいずれにも答えようとはしていませんが、あなたの質問はあなたが何か非常に危険なことをしたい、つまり、スレッド間でメモリを共有するローロックまたはロックなしのソリューションを書きたいと思っていることを指摘します。私のパフォーマンス目標を達成するために他に選択肢が本当にない場合、およびプログラム内のすべての読み取りと書き込みのすべての可能な再配列を徹底的かつ正確に分析する自信がある場合を除いて、私はそうしようとはしませんでした。あなたがこれらの質問をしているので、あなたは私が慣れているより自信が少ないと思います。
Eric Lippert

回答:


2

forループで配列を反復処理することは、C#のスレッドセーフ操作ですか?

複数のスレッドからの読み取りについて厳密に話している場合は、or ループを使用しているかどうかに関係なく、Microsoftによって作成されたほぼすべてのコレクションに対してスレッドセーフにArrayなります。特にあなたが持っている例では:List<T>forforeach

var temp = new List<int>();

foreach (var name in Names)
{
  temp.Add(name.Length * 2);
}

これは、必要な数のスレッドで実行できます。彼らはNames喜んで同じ価値観を読むでしょう。

別のスレッドから書き込んだ場合(これはあなたの質問ではありませんが、注目に値します)

反復処理するArrayList<T>forループ、それだけで読んでおこう、とあなたがそれらに遭遇として、それは喜んで変更された値を読みます。

foreachループで反復すると、実装に依存します。ループのArray途中で値が変更された場合は、foreach列挙を続け、変更された値を提供します。

ではList<T>、「スレッドセーフ」と見なすものによって異なります。正確なデータの読み取りに関心がある場合は、列挙の途中で例外がスローされ、コレクションが変更されたことが通知されるので、一種の「安全」です。しかし、例外をスローすることを安全ではないと考える場合、それは安全ではありません。

ただし、これはの設計上の決定でありList<T>、変更を明示的に検索して例外をスローするコードがあることに注意してください。設計上の決定は、次のポイントに私たちを連れて行きます:

実装するすべてのコレクションがIEnumerable複数のスレッドにわたって安全に読み取れると想定できますか?

では、ほとんどの場合、それはなりますが、スレッドセーフな読み取りは保証されません。その理由は、すべてIEnumerableがの実装を必要とするためですIEnumerator。これは、コレクション内のアイテムを走査する方法を決定します。そして、他のクラスと同じように、スレッドセーフでないものも含めて、そこでは何でも好きなことができます。

  • 静的変数の使用
  • 値を読み取るための共有キャッシュの使用
  • コレクションが列挙の途中で変更されるケースを処理するための努力をしていません

GetEnumerator()呼び出されるたびに列挙子の同じインスタンスを返すようにするなど、奇妙なことをすることもできます。これにより、予測できない結果が生じる可能性があります。

予期しない結果になる可能性がある場合は、スレッドセーフではないと考えています。これらのことは、予期しない結果を引き起こす可能性があります。

あなたは見ることができるため、ソースコードをEnumeratorそのList<T>用途あなたはそれが列挙ことを示していますその奇妙なもの、のいずれか行っていないことを確認できるように、List<T>複数のスレッドからは安全ですが。


したがって、基本的に誰かがIEnumerableのインスタンスを提供し、それについて知っている唯一のことは、IEnumerableを実装している場合、異なるスレッドからそのオブジェクトを列挙することは安全ではありません。それ以外の場合、BCLクラスのインスタンス、たとえばList <string>のインスタンスがある場合、複数のスレッドからオブジェクトを安全に列挙できます(List <string>型自体がすべてのメンバーでスレッドセーフではない場合でも、 IEnumerableインターフェイスの実装がスレッドセーフであることを確認できます)。
Enrico Massone

1
BCLコレクションの同時列挙の安全性に関するすべての説明は、仮定に基づいています。マイクロソフトから提供されたソースコードを読み取ることができ、現在のところ、同時実行に関してスレッドセーフでないBCLクラスの例はわかりません。列挙。基本的に、実装の詳細に依存しています。何かが明示的に文書化されていない限り、仮定を行うことは最も安全なオプションではありません。したがって、最も安全な選択は、問題を回避し、ImmutableArray <string>のようなものを使用することです。これは、不変性のためにスレッドセーフであることが確実です。
Enrico Massone

1
「おそらく最も安全な選択は、問題を回避し、ImmutableArray <string>のようなものを使用することです -コレクションのタイプを制御でき、スレッド間でのみ読み取りを行うことがわかっている場合、List<T>またはArrayと同じくらい安全ですImmutableArray<string>。スレッドセーフであることを証明できるためです。複数のスレッドにわたる読み取りと書き込みを計画している場合にのみ、他のオプションを調べます。から適切なコレクションを使用できますSystem.Collections.Concurrent
Gabriel Luci

1

コードがスレッドセーフであると断言するということUselessServiceは、Names配列の内容を"tom" and "jerry"または(より不吉な)のようなもので同時に置き換えようとするコードが内に存在しないことを当然のことと見なす必要があることを意味しますnull and null。一方使用することImmutableArray<string>でしょう保証するコードがスレッドセーフである、と誰もが慎重にコードの残りの部分を検査することなく、単に静的な読み取り専用フィールドの種類を見ることによって、そのことについて保証することができました。

あなたはからこれらのコメントを面白いかもしれソースコードImmutableArray<T>この構造体のいくつかの実装の詳細については、:

O(1)のインデックス可能なルックアップ時間を持つ読み取り専用配列。

この型には、サイズが1つの参照型フィールドであるという契約が文書化されています。私たち自身のSystem.Collections.Immutable.ImmutableInterlockedクラスは、それだけでなく、外部の他のクラスにも依存しています。

メンテナーとレビュアーのための重要な通知:

このタイプはスレッドセーフでなければなりません。構造体として、それは構造体を変更することができますので、そのメンバーは他のスレッドで実行されている間、一つのスレッドから変更されることから、独自のフィールドを保護することはできません場所に簡単にこの構造体を含むフィールドを再割り当てすることによって。したがって、すべてのメンバーがthisONCEを逆参照するだけであることは非常に重要ですメンバーが配列フィールドを参照する必要がある場合、それはの逆参照としてカウントされthisます。他のインスタンスメンバー(プロパティまたはメソッド)の呼び出しも逆参照としてカウントされthisます。this複数回使用する必要があるメンバーは、代わりに割り当てる必要がありますthisローカル変数に変更し、代わりに残りのコードにそれを使用します。これにより、構造体の1つのフィールドがローカル変数に効果的にコピーされ、他のスレッドから分離されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.