2013-02-01 11 views
5

Lassen Sie uns sagen, dass ich eine Klasse, die von mehreren Threads aufgerufen wird, und in dieser KlasseMit Bcl ImmutableDictionary im privaten Bereich

public class Something { 
    private ImmutableDictionary<string,string> _dict; 
    public Something() { 
     _dict = ImmutableDictionary<string,string>.Empty; 
    } 

    public void Add(string key, string value) { 

     if(!_dict.ContainsKey(key)) { 
      _dict = _dict.Add(key,value); 
     } 
    } 
} 

Könnte dies einige Daten in einem ImmutableDictionary in einem privaten Bereich speichern, werde sein so von mehreren Threads aufgerufen, dass Sie einen Fehler über den bereits im Wörterbuch vorhandenen Schlüssel bekommen?

Thread1 prüft Wörterbuch falsch sieht Thread2 prüft Wörterbuch falsch sieht Thread1 Mehrwert und Bezug auf _dict aktualisiert Thread2 Wert hinzufügt, aber es ist bereits hinzugefügt, da es die gleiche Referenz verwendet?

+1

Ja, Ich glaube, es ist nicht threadsicher in der Art, wie du es beschreibst. Sie müssen möglicherweise Ihre eigenen sperren um es herum. –

+0

In welchem ​​Szenario versuchen mehrere Threads, denselben Artikel einzufügen? Wenn Sie Ihre Arbeit parallelisieren, müssen Sie Ihre Daten über Threads/Maschinen partitionieren. –

+0

Wenn die Zuweisung war '_dict [Schlüssel] = Wert;' - vielleicht sogar die 'ContainsKey'-Prüfung entfernen - das wäre threadsicher (?) –

Antwort

3

Ja, es gilt das gleiche Rennen wie üblich (beide Threads lesen, nichts finden, dann schreiben beide Threads). Thread-Sicherheit ist keine Eigenschaft einer Datenstruktur, sondern eines ganzen Systems.

Es gibt ein anderes Problem: Gleichzeitige schreibt auf verschiedene Schlüssel wird nur verlieren schreibt.

Was Sie brauchen, ist ein ConcurrentDictionary. Sie können nicht mit dem unveränderlichen ohne eine zusätzliche Sperre oder eine CAS-Schleife arbeiten.

Update: Die Kommentare überzeugten mich, dass ein ImmutableDictionary mit einer CAS-Schleife für Schreibvorgänge ist eine sehr gute Idee, wenn Schreibvorgänge selten sind. Lese-Performance wird sehr schön sein und schreibt mit einer synchronisierten Datenstruktur so günstig wie es geht.

+0

Der Unterschied ist, mit' Dictionary' können Sie nicht CAS, Sie muss ein Schloss benutzen, was * einen Unterschied machen könnte. – svick

+0

@svick streng genommen * kann * CAS mit Wörterbuch verwenden ... Aber das ist pingelig und du hast Recht. – usr

+0

Es war mir gelungen, den doppelten Schlüssel zu umgehen, indem ich das Wörterbuch in eine lokale Variable kopierte, die Funktion ausführte und das Ergebnis dann dem privaten Feld zuordnete, aber Sie sollten beachten, dass gleichzeitige Schreibvorgänge auf verschiedene Schlüssel verloren gingen. Ich nehme an, in diesem Fall ist eine Art von ImmutableSet wahrscheinlich eine bessere Wahl und dann eine Kopie lokal und union das Ergebnis. – chrisortman

1

Der Zugriff auf die Instanzvariable macht die Add() - Methode nicht reentrant. Kopieren/Neuzuweisung an die Instanzvariable ändert nicht die Nicht-Wiederaufnahme (es ist immer noch anfällig für Race-Bedingungen). Ein ConcurrentDictionary erlaubt in diesem Fall den Zugriff ohne vollständige Konsistenz, aber auch ohne Sperren. Wenn es über Threads hinweg eine 100% ige Konsistenz geben soll (unwahrscheinlich), ist eine Art Sperre für das Dictionary notwendig. Es ist sehr wichtig zu verstehen, dass Sichtbarkeit und Bereich sind zwei verschiedene Dinge. Ob eine Instanzvariable privat ist oder nicht, hat keinen Einfluss auf ihren Umfang und damit auf ihre Threadsicherheit.

3

Sie können in Ihrem Gebrauch des unveränderlichen Wörterbuchs absolut Thread-sicher sein. Die Datenstruktur selbst ist absolut threadsicher, aber Sie müssen Änderungen in einer Umgebung mit mehreren Threads sorgfältig vornehmen, um Datenverluste in Ihrem eigenen Code zu vermeiden.

Hier ist ein Muster, das ich häufig für solch ein Szenario verwende. Es erfordert keine Sperren, da die einzige Mutation, die wir tun, eine einzelne Speicherzuweisung ist. Wenn Sie mehrere Felder festlegen müssen, müssen Sie eine Sperre verwenden.

using System.Threading; 

public class Something { 
    private ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty; 

    public void Add(string key, string value) { 
     // It is important that the contents of this loop have no side-effects 
     // since they can be repeated when a race condition is detected. 
     do { 
      var original = _dict; 
      if (local.ContainsKey(key)) { 
      return; 
      } 

      var changed = original.Add(key,value); 
      // The while loop condition will try assigning the changed dictionary 
      // back to the field. If it hasn't changed by another thread in the 
      // meantime, we assign the field and break out of the loop. But if another 
      // thread won the race (by changing the field while we were in an 
      // iteration of this loop), we'll loop and try again. 
     } while (Interlocked.CompareExchange(ref this.dict, changed, original) != original); 
    } 
} 

In der Tat, ich benutze dieses Muster so oft habe ich eine statische Methode für diesen Zweck definiert:

/// <summary> 
/// Optimistically performs some value transformation based on some field and tries to apply it back to the field, 
/// retrying as many times as necessary until no other thread is manipulating the same field. 
/// </summary> 
/// <typeparam name="T">The type of data.</typeparam> 
/// <param name="hotLocation">The field that may be manipulated by multiple threads.</param> 
/// <param name="applyChange">A function that receives the unchanged value and returns the changed value.</param> 
public static bool ApplyChangeOptimistically<T>(ref T hotLocation, Func<T, T> applyChange) where T : class 
{ 
    Requires.NotNull(applyChange, "applyChange"); 

    bool successful; 
    do 
    { 
     Thread.MemoryBarrier(); 
     T oldValue = hotLocation; 
     T newValue = applyChange(oldValue); 
     if (Object.ReferenceEquals(oldValue, newValue)) 
     { 
      // No change was actually required. 
      return false; 
     } 

     T actualOldValue = Interlocked.CompareExchange<T>(ref hotLocation, newValue, oldValue); 
     successful = Object.ReferenceEquals(oldValue, actualOldValue); 
    } 
    while (!successful); 

    Thread.MemoryBarrier(); 
    return true; 
} 

Ihre Add-Methode dann viel einfacher wird:

public class Something { 
    private ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty; 

    public void Add(string key, string value) { 
     ApplyChangeOptimistically(
      ref this.dict, 
      d => d.ContainsKey(key) ? d : d.Add(key, value)); 
    } 
}