Neues beim Multithreading mit C# ab .NET 9

Mit der Einfürung von .NET 9 hat sich auch in Bezug auf die Multithreading-Programmierung wieder etwas getan. Seit dem Beginn von .NET gibt es das Schlüsselwort "lock", dass mit beliebigen .NET-Objekten arbeiten konnte, um kritische Abschnitte im Code gegenüber einem anderem Thread zu verriegeln. Seit C# in der Version 13 wurde nun das "lock"-Schlüsselwort mit einem neuen .NET-Typen "System.Threading.Lock" ausgestattet, welches anstatt eines standardmäßigen Object-Typen verwendet werden kann. Warum sollte man denn nun diesen Typen nutzen und nicht wie bisher beispielsweise ein simples Objekt?

Abgesehen von der besseren Lesbarkeit im Code, denn die Lock-Sperre ist allein durch die Verwendung des neuen Datentyps besser zuzuordnen, ist es so, das andere Threads diese Sperre zur Laufzeit nicht mehr entriegeln können. Dies kam gerade in der Vergangenheit in Bezug auf den Einsatz der Schlüsselwörter async und await vor, wenn ein anderer Thread eine Sperre wieder aufgehoben hatte. Bildlich gesprochen ist der "lock" der Türsteher, der dafür da ist, nur eine Person gleichzeitig in den Club zu lassen. Allerdings gilt diese Sperre immer nur für einen Thread, das im Umkehrschluss bedeutet, dass auch eine doppelte Verriegelung möglich ist, z.B:

lock (_lockObject)
{
    lock (_lockObject)
    {
         // do domething
    }
}
 
Solche Konstrukte vertragen sich jedoch nicht, wenn gleichzeitig im Code mit asynchroner Programmierung mittels der Schlüsselwörter async/await gearbeitet wird. Warum ist das so? Nun, wenn lock und async/await verwendet wird, kommt es in diesem Fall in der Regel zu Deadlocks, da "lock" den aktuellen Thread blockiert und damit nur zur Synchronisation von "synchronem Code" dient. Wenn hingegen das TAP-Pattern angewendet wird, laufen separat laufende Task-Objekte im internen Task-Scheduler. Eine Lösung wäre hier die Verwendung der Klasse "SemaphoreSlim". Ich habe bereits 2018 dazu einen recht umfassenden Artikel zur asynchronen Programmierung mit C# mit entsprechende Beispielen verfasst, welcher hier verlinkt ist. 

Es war bis jetzt allerdings so, dass der Programmierer sich selbst um eine passende Auswahl alternativer Methoden zur Synchronisation der Threads kümmern musste. Der "Trick" mit dem neuen Lock-Objekt ist hier, dass nun der Compiler zur Compile-Zeit selbst die Methode wählen und intern Code generieren kann, welcher möglichst performant den jeweils kritischen Code-Abschnitt zwischen den unterschiedlichen Threads synchronisieren kann. Der Compiler ab C# in der Version 13 und .NET 9 kann bei der Synchronisation nun zwischen verschiedenen Methoden selbst wählen:

  • Die neue Lock-Struktur mit EnterScope,
  • SpinLock für kurzzeitige Sperren,
  • Interlocked-Methoden für atomare Operationen,
  • SemaphoreSlim für asynchrone Sperren

Gerade bei Anfängern, die in der Multithreading-Programmierung noch nicht sicher sind, wird dies eine enorme Erleichterung bringen Darüber hinaus sei erwähnt, dass es auch durchaus seinen Charme hat, eine AsyncLocker-Bibliothek selbst in seine Anwendung zu implementieren, die kritische Abschnitte mit dem using-Pattern verseht. Einerseits entsteht dadurch ebenfalls bereits zur Entwicklungszeit gut lesbarer Code, andererseit ist auch hier sichergestellt, dass die Synchronisation mit .NET-System übernommen und dadurch vom jeweils darunterliegenden Betriebssystem wegabstrahiert wird.

Fazit

Der interne Wechsel von Monitor zu EnterScope und Dispose in Verbindung mit der neuen Lock-Klasse bringt mit .NET 9 einerseits eine modernisierte, plattformunabhängigere, andererseits eine potenziell leistungsfähigere Alternative zur bisherigen Monitor-Klasse. Der neue Mechanismus stützt sich nicht mehr im Fall von Windows auf die Win32-API mit ihrer Methode "EnterCriticalSection", sondern nutzt .NET-interne Funktionen, was zu mehr Flexibilität und besserer Wartbarkeit führt.

Wir benutzen Cookies
Diese Seite enthält Cookies. Wir verwenden Cookies, um Inhalte und Anzeigen zu personalisieren und die Zugriffe auf unsere Website zu analysieren.