Was ist der beste Weg, um ein thread-sicheres Wörterbuch zu implementieren?


109

Ich konnte ein thread-sicheres Wörterbuch in C # implementieren, indem ich von IDictionary abgeleitet und ein privates SyncRoot-Objekt definiert habe:

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

Ich sperre dann dieses SyncRoot-Objekt in meinen Verbrauchern (mehrere Threads):

Beispiel:

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

Ich konnte es zum Laufen bringen, aber dies führte zu hässlichem Code. Meine Frage ist, gibt es eine bessere und elegantere Möglichkeit, ein thread-sicheres Wörterbuch zu implementieren?


3
Was finden Sie daran hässlich?
Asaf R

1
Ich denke, er bezieht sich auf alle Sperranweisungen, die er in seinem gesamten Code innerhalb der Konsumenten der SharedDictionary-Klasse hat - er sperrt den aufrufenden Code jedes Mal , wenn er auf eine Methode für ein SharedDictionary-Objekt zugreift.
Peter Meyer

Anstatt die Add-Methode zu verwenden, versuchen Sie es mit der Zuweisung von Werten ex- m_MySharedDictionary ["key1"] = "item1". Dies ist threadsicher.
Testbenutzer

Antworten:


43

Wie Peter sagte, können Sie die gesamte Thread-Sicherheit innerhalb der Klasse kapseln. Sie müssen bei allen Ereignissen, die Sie verfügbar machen oder hinzufügen, vorsichtig sein und sicherstellen, dass sie außerhalb von Sperren aufgerufen werden.

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

Bearbeiten: In den MSDN-Dokumenten wird darauf hingewiesen, dass die Aufzählung von Natur aus nicht threadsicher ist. Dies kann ein Grund dafür sein, ein Synchronisationsobjekt außerhalb Ihrer Klasse verfügbar zu machen. Eine andere Möglichkeit, dies zu erreichen, besteht darin, einige Methoden zum Ausführen einer Aktion für alle Mitglieder bereitzustellen und die Aufzählung der Mitglieder zu überprüfen. Das Problem dabei ist, dass Sie nicht wissen, ob die an diese Funktion übergebene Aktion ein Mitglied Ihres Wörterbuchs aufruft (dies würde zu einem Deadlock führen). Durch das Offenlegen des Synchronisationsobjekts kann der Verbraucher diese Entscheidungen treffen und den Deadlock in Ihrer Klasse nicht verbergen.


@fryguybob: Die Aufzählung war eigentlich der einzige Grund, warum ich das Synchronisationsobjekt verfügbar gemacht habe. Konventionell würde ich dieses Objekt nur dann sperren, wenn ich durch die Sammlung aufzähle.
GP.

1
Wenn Ihr Wörterbuch nicht zu groß ist, können Sie eine Kopie auflisten und diese in die Klasse integrieren.
Fryguybob

2
Mein Wörterbuch ist nicht zu groß, und ich denke, das hat den Trick getan. Ich habe eine neue öffentliche Methode namens CopyForEnum () erstellt, die eine neue Instanz eines Wörterbuchs mit Kopien des privaten Wörterbuchs zurückgibt. Diese Methode wurde dann für Aufzählungen aufgerufen und der SyncRoot wurde entfernt. Vielen Dank!
GP.

12
Dies ist auch keine inhärent threadsichere Klasse, da Wörterbuchoperationen in der Regel granular sind. Eine kleine Logik nach dem Vorbild von if (dict.Contains (was auch immer)) {dict.Remove (was auch immer); dict.Add (was auch immer, newval); } ist sicherlich eine Rennbedingung, die darauf wartet, passiert zu werden.
Sockel

207

Die .NET 4.0-Klasse, die Parallelität unterstützt, wird benannt ConcurrentDictionary.


4
Bitte markieren Sie dies als Antwort, Sie brauchen keine benutzerdefinierte Diktatur, wenn das eigene .Net eine Lösung hat
Alberto León

27
(Denken Sie daran, dass die andere Antwort lange vor der Existenz von .NET 4.0 (veröffentlicht im Jahr 2010) geschrieben wurde.)
Jeppe Stig Nielsen

1
Leider ist es keine sperrfreie Lösung, daher ist es in sicheren SQL Server CLR-Assemblys nutzlos. Sie benötigen so etwas wie das, was hier beschrieben wird: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf oder vielleicht diese Implementierung: github.com/hackcraft/Ariadne
Triynko

2
Wirklich alt, ich weiß, aber es ist wichtig zu beachten, dass die Verwendung des ConcurrentDictionary im Vergleich zu einem Dictionary zu erheblichen Leistungsverlusten führen kann. Dies ist höchstwahrscheinlich das Ergebnis einer teuren Kontextumschaltung. Stellen Sie daher sicher, dass Sie ein thread-sicheres Wörterbuch benötigen, bevor Sie eines verwenden.
Outbred

60

Der Versuch, intern zu synchronisieren, wird mit ziemlicher Sicherheit unzureichend sein, da die Abstraktionsebene zu niedrig ist. Angenommen, Sie machen die Operationen Addund ContainsKeywie folgt einzeln threadsicher:

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

Was passiert dann, wenn Sie dieses vermeintlich threadsichere Codebit aus mehreren Threads aufrufen? Wird es immer gut funktionieren?

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

Die einfache Antwort lautet nein. Irgendwann löst die AddMethode eine Ausnahme aus, die angibt, dass der Schlüssel bereits im Wörterbuch vorhanden ist. Wie kann das mit einem thread-sicheren Wörterbuch sein, könnte man fragen? Nur weil jede Operation threadsicher ist, ist die Kombination von zwei Operationen nicht möglich, da ein anderer Thread sie zwischen Ihrem Aufruf von ContainsKeyund ändern könnte Add.

Um diese Art von Szenario korrekt zu schreiben, benötigen Sie eine Sperre außerhalb des Wörterbuchs, z

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

Da Sie jedoch extern sperrenden Code schreiben müssen, verwechseln Sie die interne und externe Synchronisation, was immer zu Problemen wie unklarem Code und Deadlocks führt. Letztendlich sind Sie wahrscheinlich entweder besser:

  1. Verwenden Sie eine normale Dictionary<TKey, TValue>und synchronisieren Sie extern, indem Sie die zusammengesetzten Operationen darauf einschließen, oder

  2. Schreiben Sie einen neuen thread-sicheren Wrapper mit einer anderen Schnittstelle (dh nicht IDictionary<T>), die die Operationen wie eine AddIfNotContainedMethode kombiniert , sodass Sie niemals Operationen daraus kombinieren müssen.

(Ich neige dazu, selbst mit # 1 zu gehen)


10
Es sei darauf hingewiesen, dass .NET 4.0 eine ganze Reihe von thread-sicheren Containern wie Sammlungen und Wörterbücher enthalten wird, die eine andere Schnittstelle zur Standardsammlung haben (dh sie führen Option 2 oben für Sie aus).
Greg Beech

1
Es ist auch erwähnenswert, dass die Granularität, die selbst das grobe Sperren bietet, häufig für einen Ansatz mit mehreren Lesern und mehreren Lesern ausreicht, wenn ein geeigneter Enumerator entworfen wird, der die zugrunde liegende Klasse darüber informiert, wann sie entsorgt wird (Methoden, die das Wörterbuch währenddessen schreiben möchten) Ein nicht verfügbarer Enumerator sollte das Wörterbuch durch eine Kopie ersetzen.
Supercat

6

Sie sollten Ihr privates Sperrobjekt nicht über eine Eigenschaft veröffentlichen. Das Sperrobjekt sollte privat existieren, um ausschließlich als Treffpunkt zu fungieren.

Wenn sich die Leistung bei Verwendung des Standardschlosses als schlecht herausstellt, kann die Power Threading- Schlosssammlung von Wintellect sehr nützlich sein.


5

Es gibt verschiedene Probleme mit der von Ihnen beschriebenen Implementierungsmethode.

  1. Sie sollten Ihr Synchronisationsobjekt niemals verfügbar machen. Wenn Sie dies tun, öffnen Sie sich einem Verbraucher, der das Objekt greift und es verriegelt, und dann stoßen Sie an.
  2. Sie implementieren eine nicht threadsichere Schnittstelle mit einer threadsicheren Klasse. IMHO kostet dich das die Straße runter

Persönlich habe ich festgestellt, dass der beste Weg, eine thread-sichere Klasse zu implementieren, die Unveränderlichkeit ist. Es reduziert wirklich die Anzahl der Probleme, auf die Sie bei der Thread-Sicherheit stoßen können. Weitere Informationen finden Sie in Eric Lipperts Blog .


3

Sie müssen die SyncRoot-Eigenschaft in Ihren Consumer-Objekten nicht sperren. Die Sperre, die Sie innerhalb der Methoden des Wörterbuchs haben, ist ausreichend.

Ausarbeiten: Was am Ende passiert, ist, dass Ihr Wörterbuch für einen längeren Zeitraum als nötig gesperrt ist.

Was in Ihrem Fall passiert, ist Folgendes:

Angenommen, Thread A erhält die Sperre für SyncRoot vor dem Aufruf von m_mySharedDictionary.Add. Thread B versucht dann, die Sperre zu erlangen, ist jedoch blockiert. Tatsächlich sind alle anderen Threads blockiert. Thread A darf die Add-Methode aufrufen. Bei der lock-Anweisung innerhalb der Add-Methode darf Thread A die Sperre erneut erhalten, da sie sie bereits besitzt. Beim Verlassen des Sperrkontexts innerhalb der Methode und außerhalb der Methode hat Thread A alle Sperren freigegeben, sodass andere Threads fortgesetzt werden können.

Sie können einfach jedem Verbraucher erlauben, die Add-Methode aufzurufen, da die Lock-Anweisung in Ihrer SharedDictionary-Klasse Add-Methode den gleichen Effekt hat. Zu diesem Zeitpunkt haben Sie redundante Sperren. Sie würden SyncRoot nur außerhalb einer der Wörterbuchmethoden sperren, wenn Sie zwei Vorgänge für das Wörterbuchobjekt ausführen müssten, die garantiert nacheinander ausgeführt werden müssen.


1
Nicht wahr ... Wenn Sie zwei Operationen nacheinander ausführen, die intern threadsicher sind, bedeutet dies nicht, dass der gesamte Codeblock threadsicher ist. Zum Beispiel: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } wäre nicht threadsicher, auch wenn ContainsKey und Add threadsicher sind
Tim

Ihr Punkt ist richtig, steht aber in keinem Zusammenhang mit meiner Antwort und der Frage. Wenn Sie sich die Frage ansehen, geht es weder um das Aufrufen von ContainsKey noch um meine Antwort. Meine Antwort bezieht sich auf den Erwerb einer Sperre für SyncRoot, die im Beispiel in der ursprünglichen Frage gezeigt wird. Im Kontext der lock-Anweisung würden eine oder mehrere thread-sichere Operationen tatsächlich sicher ausgeführt.
Peter Meyer

Ich denke, wenn alles, was er jemals tut, das Hinzufügen zum Wörterbuch ist, aber da er "// mehr IDictionary-Mitglieder ..." hat, gehe ich davon aus, dass er irgendwann auch Daten aus dem Wörterbuch zurücklesen möchte. Wenn dies der Fall ist, muss ein extern zugänglicher Verriegelungsmechanismus vorhanden sein. Es spielt keine Rolle, ob es sich um den SyncRoot im Wörterbuch selbst oder um ein anderes Objekt handelt, das ausschließlich zum Sperren verwendet wird. Ohne ein solches Schema ist der Gesamtcode jedoch nicht threadsicher.
Tim

Der externe Sperrmechanismus ist wie in seinem Beispiel in der Frage gezeigt: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - Es wäre absolut sicher, Folgendes zu tun: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} Mit anderen Worten, der externe Sperrmechanismus ist die Sperranweisung, die für die öffentliche Eigenschaft SyncRoot ausgeführt wird.
Peter Meyer

0

Nur ein Gedanke, warum nicht das Wörterbuch neu erstellen? Wenn das Lesen eine Vielzahl von Schreibvorgängen ist, werden durch das Sperren alle Anforderungen synchronisiert.

Beispiel

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.