Jeder Programmierer, der mehr als einen Thread in seinem Programm verwendet, hat Synchronisationsprimitive gefunden. Im .NET-Kontext gibt es viele davon, ich werde sie nicht
auflisten ,
MSDN hat dies bereits fĂŒr mich getan.
Ich musste viele dieser Grundelemente verwenden, und sie halfen perfekt, die Aufgaben zu bewĂ€ltigen. Aber in diesem Artikel möchte ich ĂŒber die ĂŒbliche Sperre in der Desktop-Anwendung sprechen und darĂŒber, wie das neue (zumindest fĂŒr mich) Grundelement erschien, das als PriorityLock bezeichnet werden kann.
Das Problem
Bei der Entwicklung einer hoch geladenen Multithread-Anwendung wird irgendwo ein Manager angezeigt, der unzĂ€hlige Threads verarbeitet. So war es bei mir. Und dieser Manager arbeitete und verarbeitete Tonnen von Anfragen aus vielen hundert Threads. Und alles war in Ordnung mit ihm, aber im ĂŒblichen Schloss funktionierte.
Und dann klickt eines Tages ein Benutzer (zum Beispiel ich) auf eine SchaltflĂ€che in der AnwendungsoberflĂ€che, der Stream fliegt zum Manager (natĂŒrlich nicht zum UI-Stream) und erwartet einen superfreundlichen Empfang. Stattdessen wird er von Tante Klava vom dichtesten Empfang der dichtesten Klinik mit den Worten "Es ist mir egal" empfangen wer hat dich geleitet. Ich habe 950 mehr wie du. Geh und geh zu ihnen. Es ist mir egal, wie du es herausfindest. " So funktioniert die Sperre in .NET. Und alles scheint in Ordnung zu sein, alles wird korrekt ausgefĂŒhrt, aber der Benutzer hatte offensichtlich nicht vor, einige Sekunden auf eine Antwort auf seine Aktion zu warten.
Hier endet die herzzerreiĂende Geschichte und die Lösung des technischen Problems beginnt.
Lösung
Nachdem ich die Standardprimitive studiert hatte, fand ich keine geeignete Option. Aus diesem Grund habe ich beschlossen, mein Schloss zu schreiben, das einen Standardeintrag mit hoher PrioritĂ€t haben wĂŒrde. Ăbrigens habe ich nach dem Schreiben auch Nuget studiert und dort nichts Vergleichbares gefunden, obwohl ich vielleicht schlecht gesucht habe.
Um ein solches Grundelement (oder kein Grundelement mehr) zu schreiben, benötigte ich SemaphoreSlim-, SpinWait- und Interlocked-Operationen. Im Spoiler habe ich die erste Version meines PriorityLock (nur synchroner Code, aber der wichtigste) und ErklĂ€rungen dafĂŒr zitiert.
Versteckter TextIn Bezug auf die Synchronisation gibt es keine Entdeckungen, wÀhrend sich jemand im Schloss befindet, können andere nicht eintreten. Wenn eine hohe PrioritÀt erreicht wurde, wird sie von allen, die auf eine niedrige PrioritÀt warten, vorangetrieben.
LockMgr-Klasse, es wird vorgeschlagen, damit in Ihrem Code zu arbeiten. Er ist das eigentliche Objekt der Synchronisation. Erstellt Locker- und HighLocker-Objekte, enthÀlt Semaphoren, SpinWaits, ZÀhler, die in den kritischen Abschnitt gelangen möchten, den aktuellen Thread und den RekursionszÀhler.
public class LockMgr { internal int HighCount; internal int LowCount; internal Thread CurThread; internal int RecursionCount; internal readonly SemaphoreSlim Low = new SemaphoreSlim(1); internal readonly SemaphoreSlim High = new SemaphoreSlim(1); internal SpinWait LowSpin = new SpinWait(); internal SpinWait HighSpin = new SpinWait(); public Locker HighLock() { return new HighLocker(this); } public Locker Lock(bool high = false) { return new Locker(this, high); } }
Die Locker-Klasse implementiert die IDisposable-Schnittstelle. Um die Rekursion beim Erfassen einer Sperre zu implementieren, speichern wir die ID des Streams und ĂŒberprĂŒfen sie dann. AbhĂ€ngig von der PrioritĂ€t sagen wir im Falle einer hohen PrioritĂ€t sofort, dass wir gekommen sind (erhöhen Sie den HighCount-ZĂ€hler), holen Sie sich das High-Semaphor und warten Sie (falls erforderlich), um die Sperre von der niedrigen PrioritĂ€t zu lösen. Danach sind wir bereit, die Sperre zu erhalten. Im Falle einer niedrigen PrioritĂ€t erhĂ€lt das Semaphor mit niedriger PrioritĂ€t, dann warten wir auf den Abschluss aller FlĂŒsse mit hoher PrioritĂ€t und erhöhen den LowCount, indem wir das Semaphor mit hoher PrioritĂ€t fĂŒr eine Weile nehmen.
Es ist erwÀhnenswert, dass die Bedeutung von HighCount und LowCount unterschiedlich ist. HighCount zeigt die Anzahl der PrioritÀts-Threads an, die zur Sperre gekommen sind, wenn LowCount nur bedeutet, dass der Thread (eine einzelne) mit niedriger PrioritÀt in die Sperre gegangen ist.
public class Locker : IDisposable { private readonly bool _isHigh; private LockMgr _mgr; public Locker(LockMgr mgr, bool isHigh = false) { _isHigh = isHigh; _mgr = mgr; if (mgr.CurThread == Thread.CurrentThread) { mgr.RecursionCount++; return; } if (_isHigh) { Interlocked.Increment(ref mgr.HighCount); mgr.High.Wait(); while (Interlocked.CompareExchange(ref mgr.LowCount, 0, 0) != 0) mgr.HighSpin.SpinOnce(); } else { mgr.Low.Wait(); while (Interlocked.CompareExchange(ref mgr.HighCount, 0, 0) != 0) mgr.LowSpin.SpinOnce(); try { mgr.High.Wait(); Interlocked.Increment(ref mgr.LowCount); } finally { mgr.High.Release(); } } mgr.CurThread = Thread.CurrentThread; } public void Dispose() { if (_mgr.RecursionCount > 0) { _mgr.RecursionCount--; _mgr = null; return; } _mgr.RecursionCount = 0; _mgr.CurThread = null; if (_isHigh) { _mgr.High.Release(); Interlocked.Decrement(ref _mgr.HighCount); } else { _mgr.Low.Release(); Interlocked.Decrement(ref _mgr.LowCount); } _mgr = null; } } public class HighLocker : Locker { public HighLocker(LockMgr mgr) : base(mgr, true) { } }
Die Verwendung des LockMgr-Klassenobjekts war sehr prÀzise. Das Beispiel zeigt deutlich die Möglichkeit, _lockMgr innerhalb des kritischen Abschnitts wiederzuverwenden, wÀhrend die PrioritÀt nicht mehr wichtig ist.
private PriorityLock.LockMgr _lockMgr = new PriorityLock.LockMgr(); public void LowPriority() { using (_lockMgr.Lock()) { using (_lockMgr.HighLock()) {
Also habe ich mein Problem gelöst. Die Verarbeitung von Benutzeraktionen begann mit hoher PrioritÀt, niemand wurde verletzt, alle gewannen.
AsynchronitÀt
Da die Objekte der SemaphoreSlim-Klasse asynchrones Warten unterstĂŒtzen, habe ich mir diese Möglichkeit auch hinzugefĂŒgt. Der Code unterscheidet sich minimal und am Ende des Artikels werde ich einen Link zum Quellcode bereitstellen.
Hierbei ist zu beachten, dass die Task in keiner Weise an den Thread angehĂ€ngt ist. Daher kann die asynchrone Wiederverwendung der Sperre nicht auf Ă€hnliche Weise implementiert werden. DarĂŒber hinaus garantiert die von MSDN beschriebene
Task.CurrentId- Eigenschaft nichts. Hier endeten meine Optionen.
Auf der Suche nach einer Lösung bin ich auf das Projekt
NeoSmart.AsyncLock gestoĂen , in dessen Beschreibung die UnterstĂŒtzung fĂŒr die Wiederverwendung der asynchronen Sperre angegeben wurde. Technisch gesehen funktioniert die Wiederverwendung. Leider ist das Schloss selbst kein Schloss. Seien Sie vorsichtig, wenn Sie dieses Paket verwenden, beachten Sie, dass es NICHT richtig funktioniert!
Fazit
Das Ergebnis ist eine Klasse, die synchrone Operationen mit Wiederverwendung und asynchrone Operationen ohne Wiederverwendung unterstĂŒtzt. Asynchrone und synchrone Operationen können nebeneinander verwendet werden, aber nicht zusammen! Alles aufgrund der mangelnden UnterstĂŒtzung fĂŒr die Wiederverwendung der asynchronen Option.
Ich hoffe, ich bin mit solchen Problemen nicht allein und meine Lösung wird jemandem nĂŒtzlich sein. Ich habe die Bibliothek auf Github und Nuget gepostet.
Es gibt Tests im Repository, die den Zustand von PriorityLock anzeigen. Im asynchronen Teil dieses Tests wurde NeoSmart.AsyncLock getestet, und der Test ist fehlgeschlagen.
Link zum NugetGithub Link