Laden...

Threadsafe Liste, bei der ein Tasks den exklusiven Zugriff erhalten kann

Erstellt von Palladin007 vor 4 Jahren Letzter Beitrag vor 4 Jahren 2.151 Views
Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 4 Jahren
Threadsafe Liste, bei der ein Tasks den exklusiven Zugriff erhalten kann

'n Abend,

ich brauche eine Liste, die von x-beliebig vielen Threads genutzt werden kann. Soweit so gut, mein Problem ist, dass ein Task (oder mehrere) auch exklusiven Zugriff bekommen können soll. Der jeweilige Task, der den exklusiven Zugriff hat, darf beliebig lesen und schreiben, während alle Threads warten müssen, bis der Task den allgemeinen Zugriff wieder frei gibt.

Oder anderes Formuliert:
Viele Threads lesen und schreiben fleißig in einer Liste, dabei wird nie für einen längeren Zeitraum gelockt.
Parallel dazu kann ein Task (also theoretisch mehrere Threads) ebenfalls beliebig lesen oder schreiben, bis er besagten exklusiven Zugriff beantragt. Er wartet dann so lange, bis alle laufenden Aktionen oder ein anderer exklusiver Zugriff beendet sind und bekommt dann z.B. ein Handle-Objekt, mit dem er weiter arbeiten kann.

Gibt es für solche Vorhaben schon vorhandene Klassen in .NET, die das entweder können, oder vereinfachen?
Oder gibt es allgemeine Konzepte, an denen ich mich bei der Umsetzung einer eigenen Lösung orientieren kann?
Oder habe ich etwas total einfaches übersehen? 😄

Beste Grüße

S
248 Beiträge seit 2008
vor 4 Jahren

Hallo Palladin,

für mich klingt das nach ReaderWriterLock Class.
Mit dieser konnen mehrere Threads parallel aus der Liste lesen, aber immer nur ein Thread die Liste verändern.

Grüße
spooky

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 4 Jahren

Die Klasse scheint zu tun, was ich brauche, allerdings mit einem ganz entscheidenden Nachteil:
Wechselt der Thread, hab ich einen Deadlock...

Ich arbeite mit Tasks und ein simples "await Task.Delay(1000)" kann dafür sorgen, dass der darauf folgende Code in einem anderen Thread läuft und dann auf immer und ewig darauf wartet, etwas tun zu dürfen.

F
10.010 Beiträge seit 2004
vor 4 Jahren

Was ist mit der SynchronizedCollection

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 4 Jahren

Die synchronisiert ja nur die Zugriffe zwischen den Threads, ich kann aber nicht alle Threads aussperren und nur den Zugriff von einem Tasks aus erlauben.

Diese Liste bietet zwar das SyncRoot-Objekt nach draußen an, allerdings kann ich das nur begrenzt nutzen.

Im Grunde suche ich sowas:

// Thread 1 bis X:
myList.Add(123);
// oder:
lock(list.SyncRoot)
{
    var item = myList.Count + 1;

    myList.Add(count);
}

// Task:
lock (list.SyncRoot)
{
    for (var i = 0; i < 10; i++)
    {
       await Task.Yield(); // <-- Nur zum simulieren verschiedener Threads

       var item = myList.Count + i;

       myList.Add(count);
   }
}

Das funktioniert aber nicht, ein await kann ich (aus gutem Grund) nicht in einem lock nutzen.

Ich suche also sozusagen eine Art Schlüssel, der sämtliche Zugriffe sperrt, außer wenn man bei diesem Zugriff den Schlüssel mit gibt.

Meine Ideal-Vorstellung wäre sowas wie:

// Task:
using (var list2 = list.TakeLock())
{
    for (var i = 0; i < 10; i++)
    {
       await Task.Yield(); // <-- Nur zum simulieren verschiedener Threads

       var item = list2 .Count + i;

       list2 .Add(count);
   }
}

TakeLock() gibt einen Wrapper zurück, der den Zugriff regelt und beim Aufruf von Dispose() die Sperre wieder frei gibt.

S
248 Beiträge seit 2008
vor 4 Jahren

Also geht es dir primär nicht um das Synchronisieren eines konkreten Zugriffs auf eine Instanz von List<T> (oder ähnlich) sondern du möchtest eher zwei getrennte Zugriffsebenen?

Du könntest mehrere ReaderWriterLocks oder Sync-Objekte verwenden. Die "normalen" Tasks locken immer nur Ebene 1. Wenn ein Task exklusiven Zugriff möchte lockt er ebenfalls Ebene 1 und die Threads dieses Tasks locken nur Ebene 2 um sich intern zu synchronisieren.

P
441 Beiträge seit 2014
vor 4 Jahren

Lässt sich das nicht gut it einer SemaphoreSlim lösen?

-> Initialisiere die Semaphore mit einem Counter, der deiner Anzahl threads enspricht.
-> Jeder Thread der schreiben will holt sich eine Semaphore und gibt sie nach jeder Operation wieder frei
-> Will ein Thread exklusiv Zugriff, holt er sich einfach alle

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 4 Jahren

Das Problem bei den genannten Verfahren (ReaderWriterLock, Monitor bzw. lock, Semaphore/Slim) ist, dass die alle nur so lange funktionieren, wie der Thread noch der Selbe ist, wie zum Zeitpunkt, als der Zugriff beantragt wurde.
Bei Tasks muss ich aber damit rechnen, dass ich mehrere Threads habe und die fröhlich hin und her wechseln, ohne dass ich das voraus ahnen kann.

Ich denke daher nicht, dass es ohne z.B. einer Art Lock-Handle-Objekt funktioniert, also dass der Zugriff nur erlaubt wird, wenn eben dieses Handle-Objekt mit gegeben wird. Man sperrt diesen Key und jeder andere Zugriff mit dem selben Key muss warten, bis der Key wieder frei wird.

T
2.219 Beiträge seit 2008
vor 4 Jahren

@Palladin007
Warum hast du den mit den Tasks und den ConcurrentCollections deine Probleme?
Ein Task, der z.B. mit Task.Run gestartet wird, hat seinen eigenen Thread weshalb dies kein Problem sein kann.
Nur wenn du z.B. über Task.Factory.StartNew arbeitest, kann es sein, dass dieser erst im aktuellen und einige Zeit später in einem eigenen Thread läuft.

Mir wäre es aber neu, dass die ConcurrentCollections sich nicht mit den Tasks vertragen.

Nachtrag:
Versuchs mal mit der ConcurrentBag<T>

Doku

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 4 Jahren

Führe folgenden Code ein paar Mal aus:

static async Task Test()
{
    Console.WriteLine(Environment.CurrentManagedThreadId);
    await Task.Run(() => Thread.Sleep(10));
    Console.WriteLine(Environment.CurrentManagedThreadId);
    await Task.Factory.StartNew(() => Thread.Sleep(10));
    Console.WriteLine(Environment.CurrentManagedThreadId);
    await Task.Delay(10);
    Console.WriteLine(Environment.CurrentManagedThreadId);
    await Task.Yield();
    Console.WriteLine(Environment.CurrentManagedThreadId);
}

Die Ausgabe sieht immer anders aus, z.B.:

14557
14555
14446
14444
14466
14556
14564

Das Problem dabei ist, dass ich nie weiß, was bei einem await passiert. Wie arbeitet z.B. ein EFCore bei einem ToListAsync? Wie arbeitet jedes x-beliebige Framework, das asynchrone Zugriffe erlaubt, habe ich danach noch den alten Thread?

Wenn ich asynchrone Zugriffe erlauben möchte, muss ich damit rechnen, dass der Code nicht mehr im selben Thread läuft, wie vor ein paar Zeilen.

Ich hab also nichts gegen die genannten Klassen wie Semaphore/Slim oder ReaderWriterLock/Slim, aber die gehen alle davon aus, dass der Thread zwischen Begin und Ende des gelockten Codes sich nicht mehr ändert.
Klassen wie ConcurrentBag oder ConcurrentCollections synchronisieren nur den Aufruf einer einzelnen Methode, aber sobald ich den Zugriff über mehrere Aufrufe hinweg sperren möchte, versagen sie.

W
955 Beiträge seit 2010
vor 4 Jahren

* ein SemaphoreSlim verlangt nicht dass der freigebende Thread derselbe wie der sperrende Thread ist.
* was ist mit ConfigureAwait?

T
2.219 Beiträge seit 2008
vor 4 Jahren

@Palladin007
Bei Methoden wie ToListAsync wird häufig der ToList Aufruf nur per Task.Run gewrappt.
Es gibt aber auch Fälle, wo eben die gesamte Verarbeitung bei Async Methoden neu implementiert werden muss.
Hängt also von der jeweiligen Implementierung ab!

Dein Fall dürfte von .NET nicht abgedeckt sein bzw. wäre mir solch eine Collection nicht bekannt.
Ich würde vermutlich eine Wrapper Klasse bauen, die deine expliziten Zugriffe dann über ein eigenes Lock System löst.
Dein Beispielcode geht da schon in die richtige Richtung.
Das dürfte aber von der Umsetzung her nicht einfach werden und ggf. auch Fehleranfällig sein.
Den um das Thread Locking wirst du schon durch die Tasks nicht herum kommen.
Intern wirst du also auch mit einer ConcurrentCollection arbeiten müssen, sonst musst du nebem deinem expliziten Locking noch die Thread Locks auf die Collection regeln, was doppelter Aufwand wäre.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 4 Jahren

Dass es eine solche Liste nicht gibt, hab ich mir schon gedacht ^^

Ich hatte gehofft, auf sowas wie eine Monitor-Klasse gehofft, die unabhängig vom Thread arbeitet, sondern nur mit einem Objekt als Key. Entweder das Objekt ist als "In Benutzung" markiert oder eben nicht, völlig egal, in welchem Thread der Code gerade läuft.

Damit könnte ich mir dann alles selber bauen:

private object _lockKey = new object();

public void Add(T item)
{
    try
    {
        Monitor2.Lock(_lockKey);

        _innerList.Add(item);
    }
    finally
    {
        Monitor2.Release(_lockKey);
    }
}

public IDisposable TakeLock()
{
    Monitor2.Lock(_lockKey);

    return new DelegateDisposable(() => Monitor2.Release(_lockKey));
}

Das wäre nicht Mal sehr kompliziert und ließ sich beliebig auf jeden anderen Anwendungsbereich erweitern.

ein SemaphoreSlim verlangt nicht dass der freigebende Thread derselbe wie der sperrende Thread ist.

Stimmt - hatte ich nicht ganz auf dem Schirm.
Das setzt aber voraus, dass ich eine zweite List-Klasse bauen muss, die man anstelle des Originals benutzen muss, da ich in diesem Task nicht die alten Methoden (die jeweils mit Wait beginnen und Release enden) aufrufen darf.
Es wäre eine Lösung - zumindest theoretisch.

was ist mit ConfigureAwait?

Ich kann nicht zwingend davon ausgehen, dass es einen SynchronizationContext gibt, das wäre also sinnlos.