2011-01-14 13 views
10

Nel contesto di questa dichiarazione,Chiarimento di Lettura e Scrittura su un C# dizionario

Un dizionario può supportare più lettori simultaneamente, purché come la raccolta non viene modificato. Anche così, l'enumerazione tramite una raccolta non è intrinsecamente una procedura thread-safe . Nel raro caso in cui un'enumerazione contende con accessi in scrittura, la raccolta deve essere bloccata durante l'intera enumerazione . Per consentire alla collezione di accedere a più thread per la lettura e scrittura , è necessario implementare la propria sincronizzazione.

Cosa significa lettura e scrittura? La mia comprensione è che una lettura è un'operazione che cerca una chiave e fornisce un riferimento al suo valore e una scrittura è un'operazione che aggiunge o rimuove una coppia di valori chiave dal dizionario. Tuttavia, non riesco a trovare nulla di conclusivo riguardo a questo.

Quindi la grande domanda è, mentre si implementa un dizionario thread-safe, un'operazione che aggiorna il valore di una chiave esistente nel dizionario dovrebbe essere considerata un lettore o uno scrittore? Ho intenzione di avere più thread che accedono a chiavi univoche in un dizionario e di modificare i loro valori, ma i thread non aggiungeranno/rimuoveranno nuove chiavi.

L'ovvia implicazione, assumendo la modifica di un valore esistente non è un'operazione di scrittura sul dizionario, è che la mia implementazione del threaday dizione sicura può essere molto più efficiente, in quanto non avrei bisogno di ottenere un blocco esclusivo ogni volta Cerco di aggiornare il valore su una chiave esistente.

L'utilizzo di ConcurrentDictionary da .Net 4.0 non è un'opzione.

risposta

2

sovrascrivere un valore esistente dovrebbe essere trattato come un'operazione di scrittura

+2

Questo è davvero un commento, non una risposta alla domanda. Si prega di utilizzare "aggiungi commento" per lasciare un feedback all'autore. – SliverNinja

+0

@SliverNinja, mi sembra che questa risposta risolva la domanda principale "sarebbe un'operazione che aggiorna il valore di una chiave esistente nel dizionario da considerare un lettore o uno scrittore?". – binki

-1

Modifica di un valore è una scrittura e introduce una condizione di competizione.

Diciamo che il valore originale di mydict [5] = 42. Uno aggiornamenti filo mydict [5] per essere 112. Un altro thread aggiornamenti mydict [5] per essere 837.

cosa dovrebbe il valore di mydict [5] essere alla fine? L'ordine dei thread è importante in questo caso, ovvero è necessario assicurarsi che l'ordine sia esplicito o che non scriva.

+1

Quello che stai descrivendo non è in realtà una condizione di gara - dal momento che in entrambi i casi chi vince per ultimo - e che sarebbe lo stesso se hai bloccato l'accesso. Una condizione di gara sarebbe qualcosa di simile: se (! Dict.ContainsKey (5)) dict (5, new Value()) ora c'è una potenziale incoerenza generata. – Neil

+0

@Neil Penso che sia una condizione di gara, non solo una condizione di competizione che il dizionario può gestire. Sono già in codice per garantire che il tipo di condizione di gara menzionata nella risposta non avvenga. – Joshua

+3

@Joshua: potrebbe verificarsi una condizione di competizione all'interno del codice del dizionario che implementa 'mydict [5] = X', ma non esiste alcuna condizione di competizione tra le 3 istruzioni set di Serinus. Chi ha mai impostato il valore delle ultime vincite - questa è solo una descrizione della normale programmazione. Una condizione di competizione si verifica nel caso in cui si imposta lo stato in base al precedente esame di stato precedente. Da solo x = 1 non può generare una condizione di razza x ++ can. – Neil

0

Qualsiasi cosa che possa influire sui risultati di un'altra lettura deve essere considerata una scrittura.

Modifica di una chiave è più sicuramente una scrittura in quanto causerà l'elemento di muoversi nel hash interna o di un indice o comunque dizionari fanno il loro O (n log()) ... roba

Cosa si potrebbe desiderare a fare è guardare ReaderWriterLock

http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlock.aspx

+2

Gran parte della documentazione che avevo letto mostrava il blocco di Reader/Writer per avere prestazioni incredibilmente scadenti, peggio che usare un solo lucchetto. Sembra che ReaderWriterLockSlim debba essere quello che usi se è disponibile o usa qualcos'altro. http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx –

+0

@Erik -Io sono d'accordo, ma vorrei aggiungere: È sempre un equilibrio. Dovrebbe dipendere dagli altri costi nel mix. Se la quantità di lettura e scrittura è quasi uguale, consiglierei semplicemente di usare lock() - ma se la scrittura è rara - e di solito è il lock del writer reader probabilmente sta bene. Quando si aggiorna, sarà facile passare al blocco Slim. Il profilo quando si ha un problema è il modo migliore per ottenere prestazioni. Molto spesso le persone trascorrono giorni a programmare per risparmiare millisecondi per operazione. – Neil

+0

D'accordo, a volte gli sviluppatori spendono troppo cercando di pre-ottimizzare. Ho usato la frase "performance sconcertante" per indicare che sembra non essere un problema di prestazioni sottile ma piuttosto drastico. Di solito è meglio evitare di usarlo piuttosto che usarlo. Almeno, sembra che ci siano molte delle conclusioni che vedo raggiunte durante le varie discussioni sul rendimento. –

0

Aggiornare un valore è concettualmente un'operazione di scrittura. Quando si aggiorna un valore con accesso concorrente in cui viene eseguita una lettura prima che una scrittura sia completata, si legge un vecchio valore. Quando due scritture sono in conflitto, è possibile memorizzare il valore errato.

L'aggiunta di un nuovo valore potrebbe innescare una crescita della memoria sottostante. In questo caso viene allocata una nuova memoria, tutti gli elementi vengono copiati nella nuova memoria, il nuovo elemento viene aggiunto, l'oggetto dizionario viene aggiornato per fare riferimento alla nuova posizione di memoria per la memorizzazione e la vecchia memoria viene rilasciata e disponibile per la garbage collection. Durante questo periodo, più di una scrittura potrebbe causare un grosso problema. Due scritture allo stesso tempo potrebbero far scattare due istanze di questa copia di memoria. Se segui la logica, vedrai che un elemento andrà perso poiché solo l'ultimo thread per aggiornare il riferimento saprà degli elementi esistenti e non degli altri elementi che stavano cercando di essere aggiunti.

ICollection provides a member to synchronize access e il riferimento rimane valido per tutte le operazioni di crescita/contrazione.

+0

Questo è un punto interessante, se dichiaro un dizionario di e insert ("Key1", "Hello") quindi do directory ["Key1"] = "Questa è una stringa davvero molto lunga", sarebbe devi riassegnare memoria per valore, o dato che il valore è un oggetto, e la memoria è già stata allocata per l'oggetto, sarebbe in grado di riutilizzare quella memoria? Immagino che molto dipenderebbe dall'interno dell'oggetto Dizionario. – Joshua

+0

Per il dizionario lo spazio di archiviazione sottostante è ICollection >. Il fatto che tu abbia già inserito un valore significa che un record è stato aggiunto a ICollection (espandendo il suo contenuto, aumentando la memoria se necessario). Il contenuto è solo una struttura contenente un riferimento a due oggetti stringa. La modifica della stringa del valore del dizionario in qualcos'altro crea semplicemente una nuova stringa in memoria e aggiorna il riferimento KeyValuePair. Nessuna modifica alle dimensioni di archiviazione sottostanti di IDictionary. –

+0

Inoltre, Insert genera un'eccezione se si tenta di inserire la stessa chiave due volte. Questo può accadere facilmente in un ambiente multi-thread in cui il blocco non è stato implementato correttamente. Se si desidera essere più sicuri, l'accesso a una chiave inesistente causerà la sua creazione. Se esiste, verrà aggiornato. Nessun lancio eccezionale. Utilizzare Inserisci se l'aggiunta della stessa chiave è una condizione di errore effettiva che si desidera rilevare. directory.Insert ("Key1", "Hello"); versus directory ["Key1"] = "Ciao"; –

0

Un'operazione di lettura è tutto ciò che ottiene una chiave o un valore da un Dictionary, un'operazione di scrittura è qualsiasi cosa che aggiorna o aggiunge una chiave o un valore. Quindi un processo che aggiorna una chiave sarebbe considerato uno scrittore.

Un modo semplice per fare un dizionario thread-safe è quello di creare una propria implementazione di IDictionary che blocca semplicemente un mutex e poi inoltra la chiamata ad un'implementazione:

public class MyThreadSafeDictionary<T, J> : IDictionary<T, J> 
{ 
     private object mutex = new object(); 
     private IDictionary<T, J> impl; 

     public MyThreadSafeDictionary(IDictionary<T, J> impl) 
     { 
      this.impl = impl; 
     } 

     public void Add(T key, J value) 
     { 
     lock(mutex) { 
      impl.Add(key, value); 
     } 
     } 

     // implement the other methods as for Add 
} 

Si potrebbe sostituire il mutex con un lettore - Blocco del writer se si stanno avendo dei thread solo per leggere il dizionario.

Si noti inoltre che gli oggetti Dictionary non supportano la modifica delle chiavi; l'unico modo sicuro per ottenere quello che vuoi è rimuovere la coppia chiave/valore esistente e aggiungerne una nuova con la chiave aggiornata.

+0

c'è qualche ragione per farlo invece di usare ConcurrentDictionary? – mcmillab

3

Un punto importante non ancora menzionato è che se TValue è un tipo di classe, le cose detenuti da un Dictionary<TKey,TValue> saranno i identità dei TValue oggetti. Se si riceve un riferimento dal dizionario, il dizionario non saprà né si preoccuperà di ciò che si può fare con l'oggetto a cui si fa riferimento.

un utile poco classe di utilità nei casi in cui tutti i tasti associati a un dizionario sarà conosciuto in anticipo di codice che ha bisogno di usare è:

class MutableValueHolder<T> 
{ 
    public T Value; 
} 

Se uno vuole avere multi-threaded codice conta quante volte le varie stringhe appaiono in un gruppo di file, e uno sa in anticipo tutte le stringhe di interesse, si può quindi usare qualcosa come un Dictionary<string, MutableValueHolder<int>> per quello scopo. Una volta caricato il dizionario con tutte le stringhe appropriate e un'istanza MutableValueHolder<int> per ciascuna, qualsiasi numero di thread può recuperare riferimenti a oggetti MutableValueHolder<int> e utilizzare Threading.Interlocked.Increment o altri metodi simili per modificare lo Value associato a ciascuno, senza dover scrivere al Dizionario a tutti.