Qual è il modo migliore per implementare un dizionario thread-safe?


109

Sono stato in grado di implementare un dizionario thread-safe in C # derivando da IDictionary e definendo un oggetto SyncRoot privato:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

Quindi blocco questo oggetto SyncRoot in tutti i miei consumatori (più thread):

Esempio:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

Sono riuscito a farlo funzionare, ma questo ha prodotto un codice brutto. La mia domanda è: esiste un modo migliore e più elegante per implementare un dizionario thread-safe?


3
Cosa trovi di brutto al riguardo?
Asaf R

1
Penso che si riferisca a tutte le istruzioni di blocco che ha nel suo codice all'interno dei consumatori della classe SharedDictionary: sta bloccando il codice chiamante ogni volta che accede a un metodo su un oggetto SharedDictionary.
Peter Meyer

Invece di usare il metodo Add, prova a farlo assegnando valori ex- m_MySharedDictionary ["key1"] = "item1", questo è thread-safe.
testuser

Risposte:


43

Come ha detto Peter, puoi incapsulare tutta la thread safety all'interno della classe. Dovrai stare attento con tutti gli eventi che esponi o aggiungi, assicurandoti che vengano richiamati al di fuori di qualsiasi blocco.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

Modifica: i documenti MSDN sottolineano che l'enumerazione non è intrinsecamente thread-safe. Questa può essere una delle ragioni per esporre un oggetto di sincronizzazione al di fuori della tua classe. Un altro modo per affrontare questo approccio sarebbe fornire alcuni metodi per eseguire un'azione su tutti i membri e bloccare l'enumerazione dei membri. Il problema con questo è che non sai se l'azione passata a quella funzione chiama qualche membro del tuo dizionario (ciò risulterebbe in un deadlock). L'esposizione dell'oggetto di sincronizzazione consente al consumatore di prendere tali decisioni e non nasconde la situazione di stallo all'interno della classe.


@fryguybob: l'enumerazione era in realtà l'unico motivo per cui stavo esponendo l'oggetto di sincronizzazione. Per convenzione, eseguo un blocco su quell'oggetto solo quando eseguo l'enumerazione nella raccolta.
GP.

1
Se il tuo dizionario non è troppo grande puoi enumerare su una copia e averlo integrato nella classe.
fryguybob

2
Il mio dizionario non è troppo grande e penso che abbia funzionato. Quello che ho fatto è stato creare un nuovo metodo pubblico chiamato CopyForEnum () che restituisce una nuova istanza di un dizionario con copie del dizionario privato. Questo metodo è stato quindi chiamato per enumarazioni e SyncRoot è stato rimosso. Grazie!
GP.

12
Anche questa non è una classe intrinsecamente threadsafe poiché le operazioni del dizionario tendono ad essere granulari. Un po 'di logica sulla falsariga di if (dict.Contains (qualunque)) {dict.Remove (qualunque); dict.Add (qualunque, newval); } è sicuramente una condizione di gara in attesa di verificarsi.
zoccolo

207

Viene denominata la classe .NET 4.0 che supporta la concorrenza ConcurrentDictionary.


4
Si prega di contrassegnarlo come risposta, non è necessario un dittoniario personalizzato se il proprio .Net ha una soluzione
Alberto León

27
(Ricorda che l'altra risposta è stata scritta molto prima dell'esistenza di .NET 4.0 (rilasciato nel 2010).)
Jeppe Stig Nielsen

1
Purtroppo non è una soluzione priva di blocchi, quindi è inutile negli assembly sicuri di SQL Server CLR. Avresti bisogno di qualcosa come quello descritto qui: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf o forse questa implementazione: github.com/hackcraft/Ariadne
Triynko

2
Veramente vecchio, lo so, ma è importante notare che l'utilizzo di ConcurrentDictionary rispetto a un dizionario può comportare perdite significative di prestazioni. Questo è molto probabilmente il risultato di un costoso cambio di contesto, quindi assicurati di aver bisogno di un dizionario thread-safe prima di usarne uno.
superato il

60

Il tentativo di sincronizzarsi internamente sarà quasi certamente insufficiente perché è a un livello di astrazione troppo basso. Supponiamo di rendere le operazioni Adde ContainsKeyindividualmente thread-safe come segue:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

Allora cosa succede quando chiami questo bit di codice presumibilmente thread-safe da più thread? Funzionerà sempre bene?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

La semplice risposta è no. Ad un certo punto il Addmetodo genererà un'eccezione che indica che la chiave esiste già nel dizionario. Come può essere con un dizionario thread-safe, potresti chiedere? Bene, solo perché ogni operazione è thread-safe, la combinazione di due operazioni non lo è, poiché un altro thread potrebbe modificarla tra la chiamata a ContainsKeye Add.

Il che significa che per scrivere correttamente questo tipo di scenario è necessario un blocco esterno al dizionario, ad es

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

Ma ora, visto che devi scrivere codice di blocco esterno, stai confondendo la sincronizzazione interna ed esterna, il che porta sempre a problemi come codice non chiaro e deadlock. Quindi alla fine probabilmente sei meglio:

  1. Usa una normale Dictionary<TKey, TValue>e sincronizza esternamente, racchiudendo le operazioni composte su di essa, o

  2. Scrivi un nuovo wrapper thread-safe con un'interfaccia diversa (cioè no IDictionary<T>) che combini le operazioni come un AddIfNotContainedmetodo in modo da non dover mai combinare le operazioni da esso.

(Tendo ad andare con # 1 me stesso)


10
Vale la pena sottolineare che .NET 4.0 includerà un intero gruppo di contenitori thread-safe come raccolte e dizionari, che hanno un'interfaccia diversa dalla raccolta standard (cioè stanno facendo l'opzione 2 sopra per te).
Greg Beech

1
Vale anche la pena notare che la granularità offerta anche da un blocco grezzo sarà spesso sufficiente per un approccio a più lettori con un solo scrittore se si progetta un enumeratore adatto che consenta alla classe sottostante di sapere quando viene eliminata (metodi che vorrebbero scrivere il dizionario mentre esiste un enumeratore non smaltito dovrebbe sostituire il dizionario con una copia).
supercat

6

Non dovresti pubblicare il tuo oggetto di blocco privato tramite una proprietà. L'oggetto lock dovrebbe esistere privatamente al solo scopo di fungere da punto di incontro.

Se le prestazioni si dimostrano scadenti utilizzando il blocco standard, la raccolta di blocchi Power Threading di Wintellect può essere molto utile.


5

Ci sono diversi problemi con il metodo di implementazione che stai descrivendo.

  1. Non dovresti mai esporre il tuo oggetto di sincronizzazione. In questo modo ti aprirai a un consumatore che afferra l'oggetto e lo blocca e poi sei tostato.
  2. Stai implementando un'interfaccia non thread-safe con una classe thread-safe. IMHO questo ti costerà lungo la strada

Personalmente, ho trovato che il modo migliore per implementare una classe thread-safe è tramite l'immutabilità. Riduce davvero il numero di problemi che puoi incontrare con la sicurezza dei thread. Controlla il blog di Eric Lippert per maggiori dettagli.


3

Non è necessario bloccare la proprietà SyncRoot negli oggetti consumer. Il blocco che hai nei metodi del dizionario è sufficiente.

Per elaborare: ciò che finisce per accadere è che il tuo dizionario è bloccato per un periodo di tempo più lungo del necessario.

Quello che succede nel tuo caso è il seguente:

Supponiamo che il thread A acquisisca il blocco su SyncRoot prima della chiamata a m_mySharedDictionary.Add. Il thread B tenta quindi di acquisire il blocco ma viene bloccato. In effetti, tutti gli altri thread sono bloccati. Il thread A può chiamare il metodo Add. Nell'istruzione lock all'interno del metodo Add, il thread A può ottenere di nuovo il lock perché lo possiede già. All'uscita dal contesto di blocco all'interno del metodo e quindi all'esterno del metodo, il thread A ha rilasciato tutti i blocchi consentendo agli altri thread di continuare.

Puoi semplicemente consentire a qualsiasi consumatore di chiamare il metodo Add poiché l'istruzione lock all'interno della tua classe SharedDictionary, il metodo Add avrà lo stesso effetto. A questo punto, hai un blocco ridondante. Blocceresti solo SyncRoot al di fuori di uno dei metodi del dizionario se dovessi eseguire due operazioni sull'oggetto dizionario che dovevano essere garantite che si verifichino consecutivamente.


1
Non è vero ... se si eseguono due operazioni dopo l'altra internamente thread-safe, ciò non significa che il blocco di codice complessivo sia thread-safe. Ad esempio: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } non sarebbe threadsafe, anche se ContainsKey e Add sono threadsafe
Tim

Il tuo punto è corretto, ma è fuori contesto con la mia risposta e la domanda. Se guardi la domanda, non parla di chiamare ContainsKey, né la mia risposta. La mia risposta si riferisce all'acquisizione di un blocco su SyncRoot che è mostrato nell'esempio nella domanda originale. Nel contesto dell'istruzione lock una o più operazioni thread-safe verrebbero effettivamente eseguite in modo sicuro.
Peter Meyer

Immagino che se tutto ciò che farà è aggiungere al dizionario, ma poiché ha "// più membri IDictionary ...", presumo che a un certo punto vorrà anche rileggere i dati dal dizionario. Se questo è il caso, è necessario che ci sia un meccanismo di blocco accessibile dall'esterno. Non importa se è la SyncRoot nel dizionario stesso o un altro oggetto utilizzato esclusivamente per il blocco, ma senza un tale schema, il codice complessivo non sarà thread-safe.
Tim

Il meccanismo di blocco esterno è come mostra il suo esempio nella domanda: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - sarebbe perfettamente sicuro fare quanto segue: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} In altre parole, il meccanismo di blocco esterno è l'istruzione lock che opera sulla proprietà pubblica SyncRoot.
Peter Meyer

0

Solo un pensiero perché non ricreare il dizionario? Se la lettura è una moltitudine di operazioni di scrittura, il blocco sincronizzerà tutte le richieste.

esempio

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.