2009-07-02 17 views
8

Ich habe eine Situation in C#, wo ich eine Liste von einfachen Typen habe. Auf diese Liste kann von mehreren Threads zugegriffen werden: Einträge können hinzugefügt oder entfernt werden, und das Vorhandensein eines Eintrags kann überprüft werden. Ich habe die Liste in einem Objekt verkapselt, das bisher nur diese drei Operationen offenlegt.C# Concurrent List Fragen

Ich habe ein paar Fälle zu behandeln (nicht genau die gleichen wie die Methoden, die ich gerade erwähnt habe).
1. Ein Thread kann nur auf das Vorhandensein eines Eintrags prüfen. (einfach)
2. Ein Thread kann nach dem Vorhandensein eines Eintrags suchen, und falls dieser nicht existiert, fügen Sie ihn hinzu.
3. Ein Thread muss prüfen, ob ein Eintrag vorhanden ist, und wenn dies der Fall ist, warten Sie, bis er entfernt wurde.
4. Eine Kombination von 2 und 3, bei der ein Thread nach dem Vorhandensein eines Eintrags sucht, falls dieser existiert, muss er warten, bis er entfernt wird, bevor er ihn selbst hinzufügen kann.

Die ganze Idee ist, dass die Existenz eines Eintrags eine Sperre bedeutet. Wenn ein Eintrag vorhanden ist, kann das Objekt, das er identifiziert, nicht geändert werden, und der Code kann nicht fortgesetzt werden, da er an anderer Stelle geändert wird.

Das mag wie eine einfache Anfänger Situation erscheinen, aber ich bin auf Nebenläufigkeit Probleme und es macht mich ein bisschen paranoid, und ich bin auch nicht so vertraut mit C# Nebenläufigkeit Mechanismen.

Was wäre der beste Weg, damit umzugehen? Bin ich total ausgeschaltet? Sollte check und add (test und set?) Zu einer vierten atomaren Operation kombiniert werden? Würde ich meinen Methoden einfach Sperrblöcke hinzufügen, auf die die Liste zugreift?

Ist es auch möglich, Einheiten dieser Art zu testen (nicht die einfachen Operationen, die Nebenläufigkeitssituationen)?

Antwort

8

Komponententests werden sicherlich schwierig sein.

Dies alles kann vernünftigerweise einfach mit den "nativen" Gleichzeitigkeitsmechanismen in .NET: lock-Anweisungen und Monitor.Wait/Monitor.PulseAll getan werden. Sofern Sie nicht über einen separaten Monitor pro Element verfügen, müssen Sie alle Threads aufwecken, sobald etwas entfernt wird - andernfalls können Sie dem "richtigen" Thread nicht mehr sagen, dass er aufwachen soll.

Wenn es wirklich nur ein gesetzt von Elementen, könnten Sie HashSet<T> statt List<T> zu repräsentieren die Sammlung, übrigens verwenden wollen - nichts, was Sie erwähnt haben, ist bei der Bestellung zu tun.

Beispielcode, unter der Annahme, dass ein Satz für Sie in Ordnung ist:

using System; 
using System.Collections.Generic; 
using System.Threading; 

public class LockCollection<T> 
{ 
    private readonly HashSet<T> items = new HashSet<T>(); 
    private readonly object padlock = new object(); 

    public bool Contains(T item) 
    { 
     lock (padlock) 
     { 
      return items.Contains(item); 
     } 
    } 

    public bool Add(T item) 
    { 
     lock (padlock) 
     { 
      // HashSet<T>.Add does what you want already :) 
      // Note that it will return true if the item 
      // *was* added (i.e. !Contains(item)) 
      return items.Add(item); 
     } 
    } 

    public void WaitForNonExistence(T item) 
    { 
     lock (padlock) 
     { 
      while (items.Contains(item)) 
      { 
       Monitor.Wait(padlock); 
      } 
     } 
    } 

    public void WaitForAndAdd(T item) 
    { 
     lock (padlock) 
     { 
      WaitForNonExistence(item); 
      items.Add(item); 
     } 
    } 

    public void Remove(T item) 
    { 
     lock (padlock) 
     { 
      if (items.Remove(item)) 
      { 
       Monitor.PulseAll(padlock); 
      } 
     } 
    } 
} 

(. Völlig ungetestet, zugegebenermaßen Sie mögen vielleicht auch Timeouts für den Wartecode angeben ...)

+0

+1, kam ich zu einer ähnlichen Lösung, aber die HashSet macht sie effizienter. Nette Demo für die Monitor-Klasse, mit verschachtelten Locks etc. Nur ein Problem kann ein "Ansturm" sein, wenn ein Gegenstand entfernt wird. –

8

Während # 1 kann am einfachsten zu schreiben sein, es ist im Grunde eine nutzlose Methode. Wenn Sie nicht nach Abschluss einer Abfrage für "Vorhandensein eines Eintrags" dieselbe Sperre beibehalten, geben Sie tatsächlich "Existenz eines Eintrags zu einem bestimmten Zeitpunkt in der Vergangenheit" zurück. Es gibt Ihnen keine Informationen über die aktuelle Existenz des Eintrags.

Zwischen der Entdeckung eines Wertes in der Liste, dann jede Operation zum Abrufen, entfernen Sie den Wert, könnte ein anderer Thread kommen und es für Sie entfernen.

Die Operationen für eine gleichzeitige Liste sollten mit der Operation kombiniert werden, die Sie im Falle einer echten/falschen Existenz dieser Prüfung durchführen möchten.Zum Beispiel TestAdd() oder TestRemove() ist viel sicherer als Enthält + Hinzufügen oder Enthält + Entfernen

+0

Sie sollten einen Link zu Ihrem Blogbeitrag zu Threadsafe-Erfassungsschnittstellen erstellen. –

+0

Ja, aber # 4 tut genau das. Die Verwendbarkeit der anderen Methoden ist zweifelhaft, aber nicht unbedingt falsch. –

+0

@Henk, ich denke, # 2- # 4 sind gute Methoden. # 1 ist zwar nicht falsch, aber es ist einfach nicht nützlich. – JaredPar

1

Es gibt ein Produkt zum Finden von Race Conditions und dergleichen in Komponententests. Es heißt TypeMock Racer. Ich kann jedoch nichts dafür oder dagegen sagen. :)