2013-02-01 19 views
5

Diciamo che ho una classe che verrà chiamato da più thread, e sto andando a memorizzare alcuni dati in un ImmutableDictionary in un settore privato in questa classeUtilizzando Bcl ImmutableDictionary in ambito privato

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

Potrebbe essere questa chiamato in tal modo da più thread che si otterrà un errore sulla chiave già esistente nel dizionario?

Thread1 controlla dizionario vede falsa Thread2 controlla dizionario vede falsa Thread1 aggiunge valore e riferimento alla _dict viene aggiornato Thread2 aggiunge valore, ma si è già aggiunto perché utilizza lo stesso riferimento?

+1

Sì, Credo che non sia infallibile nel modo in cui descrivi. Potrebbe essere necessario fare il proprio blocco intorno ad esso. –

+0

Qual è lo scenario in cui sono presenti più thread che tentano di inserire lo stesso elemento? Se si intende parallelizzare il proprio lavoro, è necessario suddividere i dati su thread/macchine. –

+0

Se l'assegnazione era '_dict [chiave] = valore;' - forse anche rimuovendo il controllo 'ContainsKey' - sarebbe thread-safe (?) –

risposta

3

, si applica la stessa corsa del solito (entrambi i thread letti, non trovano nulla, quindi entrambi i thread scrivono). La sicurezza del thread non è una proprietà di una struttura dati ma di un intero sistema.

C'è un altro problema: le scritture simultanee su chiavi diverse solo perdono le scritture.

Quello che ti serve è un ConcurrentDictionary. Non è possibile eseguire questo lavoro con l'immutabile senza un blocco aggiuntivo o un ciclo CAS.

Aggiornamento: I commenti mi ha convinto che un ImmutableDictionary utilizzato con un CAS-loop per la scrittura è in realtà una buona idea se le scritture non sono frequenti. Leggere le prestazioni sarà molto bello e scrive a un prezzo economico come con una struttura dati sincronizzata.

+0

La differenza è, con' Dictionary', non puoi usare CAS, tu devi usare un lucchetto, che * potrebbe * fare la differenza. – svick

+0

@svick a rigor di termini * puoi * usare CAS con Dizionario ... Ma questo è il pignolo e hai ragione. – usr

+0

Ero riuscito a aggirare la chiave duplicata copiando il dizionario su una variabile locale, eseguendo la funzione e assegnando il risultato al campo privato, ma si fa un buon punto che le scritture simultanee su chiavi diverse andrebbero perse. Suppongo che in questo caso una sorta di ImmutableSet sia probabilmente una scelta migliore e quindi eseguire una copia locale e unione del risultato. – chrisortman

1

L'accesso alla variabile di istanza rende il metodo Add() non rientranti. Copia/riassegna alla variabile di istanza non cambia la non rientranza (è ancora soggetta alle condizioni di gara). Un ConcurrentDictionary in questo caso consentirà l'accesso senza coerenza totale, ma anche senza blocco. Se c'è bisogno di coerenza al 100% attraverso i thread (improbabile), allora è necessario un qualche tipo di blocco sul dizionario. È molto importante capire che la visibilità di e lo scope sono due cose diverse. Se una variabile di istanza è privata o meno non ha alcun rapporto con il suo ambito e quindi sulla sua sicurezza del thread.

3

Si può assolutamente essere thread-safe nell'uso del dizionario immutabile. La stessa struttura dati è perfettamente thread-safe, ma è necessario scrivere attentamente le modifiche ad esso in un ambiente multi-thread per evitare la perdita di dati nel proprio codice.

Ecco uno schema che uso frequentemente solo per uno scenario del genere. Non richiede serrature, poiché l'unica mutazione che facciamo è un singolo incarico di memoria. Se devi impostare più campi, devi usare un lucchetto.

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 realtà, io uso questo modello così spesso ho definito un metodo statico per questo scopo:

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

tuo metodo Add allora diventa molto più semplice:

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