Ist das Iterieren über ein Array mit einer for-Schleife eine thread-sichere Operation in C #? Was ist mit dem Iterieren eines IEnumerable <T> mit einer foreach-Schleife?

8

Basierend auf meinem Verständnis ein C # Array gegeben, der Akt der gleichzeitig von mehreren Threads über das Array Iterieren ist ein Thread einen sicheren Betrieb.

Mit Iteration über das Array meine ich das Lesen aller Positionen innerhalb des Arrays mittels einer einfachen alten forSchleife . Jeder Thread liest einfach den Inhalt eines Speicherplatzes innerhalb des Arrays. Niemand schreibt etwas, sodass alle Threads auf konsistente Weise dasselbe lesen.

Dies ist ein Stück Code, der das tut, was ich oben geschrieben habe:

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;
   }
}

Mein Verständnis ist also, dass die Methode DoSomethingUseless threadsicher ist und dass es nicht erforderlich ist , die string[]durch einen threadsicheren Typ zu ersetzen (wie ImmutableArray<string>zum Beispiel).

Hab ich recht ?

Nehmen wir nun an, wir haben eine Instanz von IEnumerable<T>. Wir wissen nicht, was das zugrunde liegende Objekt ist, wir wissen nur, dass ein Objekt implementiert ist IEnumerable<T>, sodass wir es mithilfe der foreachSchleife durchlaufen können .

Nach meinem Verständnis gibt es in diesem Szenario keine Garantie dafür, dass das gleichzeitige Durchlaufen dieses Objekts von mehreren Threads eine threadsichere Operation ist. Anders ausgedrückt, es ist durchaus möglich, dass das gleichzeitige Durchlaufen der IEnumerable<T>Instanz von verschiedenen Threads den internen Status des Objekts unterbricht, sodass es beschädigt wird.

Bin ich in diesem Punkt richtig?

Was ist mit der IEnumerable<T>Implementierung der ArrayKlasse? Ist es threadsicher?

Anders ausgedrückt, ist der folgende Code-Thread sicher? (Dies ist genau der gleiche Code wie oben, aber jetzt wird das Array durch Verwendung einer foreachSchleife anstelle einer forSchleife iteriert. )

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;
   }
}

Gibt es eine Referenz, die angibt, welche IEnumerable<T>Implementierungen in der .NET-Basisklassenbibliothek tatsächlich threadsicher sind?

Enrico Massone
quelle
4
@Christopher: Re: Ihr erster Kommentar: In Enricos Szenario, in dem sich ein Array befindet nur gelesen wird und weder die Speicherung der Array-Referenz noch die Elemente des Arrays mutiert werden, sollte ein Szenario mit mehreren Lesern ohne zusätzliche Barrieren sicher sein.
Eric Lippert
3
@Christopher "Nein, das Iterieren über ein Array ist kein Thread-Speichern." - Das Iterieren über ein Array (dh Array) ist threadsicher, solange alle Threads es nur lesen. Gleiches List<T>gilt für und wahrscheinlich für jede andere von Microsoft geschriebene Sammlung. Es ist jedoch möglich, eigene zu erstellen IEnumerable, die nicht threadsichere Dinge im Enumerator ausführen. Daher ist es keine Garantie dafür, dass alles, was implementiert IEnumerablewird, threadsicher ist.
Gabriel Luci
6
@Christopher: Re: Ihr zweiter Kommentar: Nein, foreachfunktioniert mit mehr als nur Enumeratoren. Der Compiler ist intelligent genug, um zu wissen, dass er bei statischen Typinformationen, die darauf hinweisen, dass es sich bei der Auflistung um ein Array handelt, die Generierung des Enumerators überspringt und direkt auf das Array zugreift, als wäre es eine forSchleife. Wenn die Sammlung eine benutzerdefinierte Implementierung von unterstützt GetEnumerator, wird diese Implementierung ebenfalls verwendet, anstatt sie aufzurufen IEnumerable.GetEnumerator. Die Vorstellung, dass foreachnur Menschenhandel stattfindet, IEnumerable/IEnumeratorist falsch.
Eric Lippert
3
@Christopher: Re: Ihr dritter Kommentar: Seien Sie vorsichtig. Obwohl es so etwas wie "abgestanden" geben mag, ist die Vorstellung, dass es daher auch so etwas wie "frisch" gibt, nicht richtig. volatilegarantiert nicht, dass Sie die aktuellste Aktualisierung einer Variablen erhalten, da C # es verschiedenen Threads ermöglicht, unterschiedliche Lese- und Schreibreihenfolgen zu beobachten, und daher der Begriff "frischeste" nicht global definiert ist . Anstatt über Frische nachzudenken, sollten Sie darüber nachdenken, welche Einschränkungen volatile die Nachbestellung von Schreibvorgängen einschränken .
Eric Lippert
6
@EnricoMassone: Ich versuche nicht, eine Ihrer Fragen zu beantworten, aber ich stelle fest, dass Ihre Fragen darauf hinweisen, dass Sie etwas sehr Riskantes tun möchten, nämlich eine Low-Lock- oder No-Lock-Lösung zu schreiben, die den Speicher über Threads hinweg teilt. Ich würde es nicht versuchen, außer in Fällen, in denen es wirklich keine andere Wahl gab, die meine Leistungsziele erfüllte, und in Fällen, in denen ich zuversichtlich war, alle möglichen Neuordnungen aller Lese- und Schreibvorgänge im Programm gründlich und genau zu analysieren . Da Sie diese Fragen stellen, fühlen Sie sich meiner Meinung nach weniger sicher, als ich es gerne hätte.
Eric Lippert

Antworten:

2

Ist das Iterieren über ein Array mit einer for-Schleife eine thread-sichere Operation in C #?

Wenn Sie ausschließlich über das Lesen aus mehreren Threads sprechen , ist dies threadsicherArray undList<T> nahezu jede von Microsoft geschriebene Sammlung , unabhängig davon, ob Sie eine foroder eine foreachSchleife verwenden. Besonders in dem Beispiel, das Sie haben:

var temp = new List<int>();

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

Sie können dies über so viele Threads tun, wie Sie möchten. Sie werden alle Namesglücklich die gleichen Werte lesen .

Wenn Sie aus einem anderen Thread darauf schreiben (dies war nicht Ihre Frage, aber es ist erwähnenswert)

Iterieren über eine Array oder List<T>mit einer forSchleife , wird einfach weiter gelesen, und die geänderten Werte werden gerne gelesen, wenn Sie auf sie stoßen.

Iterieren mit einem foreach Schleife , hängt dies von der Implementierung ab. Wenn sich ein Wert auf Arrayhalbem Weg durch eine foreachSchleife ändert , werden nur die Aufzählungen fortgesetzt und Sie erhalten die geänderten Werte.

Mit List<T>kommt es darauf an, was Sie als "threadsicher" betrachten. Wenn Sie sich mehr mit dem Lesen genauer Daten befassen, ist dies "sicher", da es eine Ausnahme in der Mitte der Aufzählung auslöst und Ihnen mitteilt, dass sich die Sammlung geändert hat. Wenn Sie jedoch eine Ausnahme als nicht sicher betrachten, ist sie nicht sicher.

Es ist jedoch erwähnenswert, dass dies eine Designentscheidung in ist List<T> ist. Es gibt Code , der explizit nach Änderungen sucht und eine Ausnahme auslöst. Designentscheidungen bringen uns zum nächsten Punkt:

Können wir davon ausgehen, dass jeder implementierte Sammlung IEnumerablesicher über mehrere Threads hinweg gelesen werden kann?

In den meisten Fällen ist dies der Fall, aber das threadsichere Lesen ist nicht garantiert. Der Grund dafür ist, dass für jede IEnumerableImplementierung eine Implementierung erforderlich ist IEnumerator, die entscheidet, wie die Elemente in der Sammlung durchlaufen werden. Und genau wie in jeder Klasse können Sie alles tun, was Sie wollen , einschließlich nicht threadsicherer Dinge wie:

  • Statische Variablen verwenden
  • Verwenden eines gemeinsam genutzten Caches zum Lesen von Werten
  • Keine Anstrengungen unternehmen, um Fälle zu behandeln, in denen sich die Sammlung während der Aufzählung ändert
  • usw.

Sie können sogar etwas Seltsames tun, z. GetEnumerator()B. jedes Mal, wenn der Enumerator aufgerufen wird, dieselbe Instanz zurückgeben. Das könnte wirklich zu unvorhersehbaren Ergebnissen führen.

Ich halte etwas für nicht threadsicher, wenn es zu unvorhersehbaren Ergebnissen führen kann. Jedes dieser Dinge kann zu unvorhersehbaren Ergebnissen führen.

Sie können sehen , den Quellcode für die , Enumeratordass List<T>Anwendungen , so können Sie sehen , dass es nicht dieses seltsamen Ding tut, die Ihnen sagt , dass Aufzählen List<T>von mehreren Threads sicher ist.

Gabriel Luci
quelle
so dass im Grunde , wenn jemand Sie eine Instanz von IEnumerable und das gibt nur , was Sie darüber wissen ist , dass es IEnumerable implementiert, dann ist es nicht sicher , dass Objekt aus verschiedenen Threads aufzuzählen. Andernfalls können Sie, wenn Sie eine Instanz einer BCL-Klasse haben, beispielsweise eine Instanz von List <string>, das Objekt sicher aus mehreren Threads auflisten (auch wenn der List <string> -Typ selbst nicht in allen seinen Mitgliedern threadsicher ist) kann sicher sein, dass die Implementierung der IEnumerable-Schnittstelle threadsicher ist).
Enrico Massone
1
Die gesamte Diskussion über die Sicherheit gleichzeitiger Aufzählungen von BCL-Sammlungen basiert auf der Annahme: Wir können den von Microsoft bereitgestellten Quellcode lesen und kennen derzeit kein Beispiel für eine BCL-Klasse, die in Bezug auf gleichzeitige nicht threadsicher ist Aufzählung. Grundsätzlich verlassen wir uns auf ein Implementierungsdetail. Sofern etwas nicht explizit dokumentiert ist, ist die Annahme nicht die sicherste Option. Die sicherste Wahl ist also wahrscheinlich, Probleme zu vermeiden und so etwas wie einen ImmutableArray <string> zu verwenden, von dem wir sicher wissen, dass er aufgrund seiner Unveränderlichkeit threadsicher ist.
Enrico Massone
1
„die sicherste Wahl wahrscheinlich ist die Vermeidung von Störungen und mit so etwas wie ein ImmutableArray <string>“ - Wenn Sie die Kontrolle über die Art der Sammlung, und Sie wissen , dass Sie nur über Threads lesen, dann List<T>oder Arrayist nur so sicher wie ImmutableArray<string>, da wir beweisen können, dass diese threadsicher zu lesen sind. Ich würde mich nur mit anderen Optionen befassen, wenn Sie vorhaben , über mehrere Threads hinweg zu lesen und zu schreiben . Sie könnten eine entsprechende Sammlung von verwenden System.Collections.Concurrent.
Gabriel Luci
1

Um zu behaupten, dass Ihr Code threadsicher ist, müssen wir Ihre Worte als selbstverständlich betrachten, dass sich kein Code im Code befindet, der UselessServicegleichzeitig versucht, den Inhalt des NamesArrays durch so etwas zu ersetzen"tom" and "jerry" oder (unheimlicher)null and null . Auf der anderen Seite ImmutableArray<string>würde die Verwendung eines garantieren dass der Code threadsicher ist, und jeder kann sich darauf verlassen, wenn er nur den Typ des statischen schreibgeschützten Felds betrachtet, ohne den Rest des Codes sorgfältig prüfen zu müssen.

Sie können diese Kommentare aus dem interessant finden Quellcode vonImmutableArray<T> , was einige Implementierungsdetails dieser Struktur betrifft:

Ein schreibgeschütztes Array mit einer indizierbaren Suchzeit von O (1).

Dieser Typ hat einen dokumentierten Vertrag, der genau ein Feld vom Typ Referenztyp ist. Unser eigenesSystem.Collections.Immutable.ImmutableInterlocked Klasse hängt davon ab, ebenso wie andere von außen.

WICHTIGER HINWEIS FÜR WARTER UND ÜBERPRÜFER:

Dieser Typ sollte threadsicher sein. Als Struktur kann es seine eigenen Felder nicht davor schützen, von einem Thread geändert zu werden, während seine Mitglieder auf anderen Threads ausgeführt werden, da Strukturen einfach durch Neuzuweisung des Felds, das diese Struktur enthält, an Ort und Stelle geändert werden können . Daher ist es äußerst wichtig, dass jedes Mitglied nur EINMAL dereferenziert this. Wenn ein Mitglied auf das Array-Feld verweisen muss, gilt dies als Dereferenzierung von this. Das Aufrufen anderer Instanzmitglieder (Eigenschaften oder Methoden) zählt ebenfalls als Dereferenzierung this. Jedes Mitglied, das thismehr als einmal verwenden muss, muss stattdessen zuweisenthiszu einer lokalen Variablen und verwenden Sie diese stattdessen für den Rest des Codes. Dadurch wird das eine Feld in der Struktur effektiv in eine lokale Variable kopiert, sodass es von anderen Threads isoliert ist.

Theodor Zoulias
quelle