Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

C#-Locking

Damit parallel laufende Threads nicht zu falschen Ergebnissen führen.
© dotnetpro
Multithreading ist immer ein spannendes Thema. Der Begriff – häufig auch als Nebenläufigkeit bezeichnet – steht in der Informatik für das gleichzeitige (oder quasi­gleichzeitige) Abarbeiten mehrerer Threads (Ausführungsstränge) innerhalb eines einzelnen Prozesses oder eines Tasks [1]. Im Gegensatz zum Multitasking, bei dem mehrere unabhängige Programme voneinander abgeschottet quasigleichzeitig ausgeführt werden, sind die Threads eines Anwendungsprogramms nicht voneinander abgeschottet und können somit durch sogenannte Race Conditions Fehler verursachen.Wenn sich mehrere Threads gleichzeitig um eine Ressource streiten, kommt deshalb das Thema Locking mit C# ins Spiel. Doch was steckt eigentlich hinter einem solchen Lock, und was ändert sich mit der neuesten C#-Version 13?

Warum Locking eine Rolle spielt

Unter einem Lock oder Locking (englisch für Sperre oder Sperren) versteht man in der Informatik das Sperren des Zugriffs auf ein Betriebsmittel [2]. Eine solche Sperre ermöglicht den exklusiven Zugriff eines Prozesses auf eine Ressource, es wird garantiert, dass kein anderer Prozess diese Ressource liest oder verändert, solange die Sperre besteht.Das Grundproblem beim Thema Locking soll anhand der Beispielklasse Konto (Bild 1) demonstriert werden. Die Klasse besitzt ein öffentliches Feld namens kontostand vom Typ int. Die Beispielanwendung startet über den ThreadPool zwei Aufrufe derselben Methode Konto.Manipulate, welche den Kontostand kontinuierlich erhöht. In einer idealen Welt würde die Methode dazu führen, dass der Kontostand auf eine Million anwächst.
Die Beispielklasse Konto mit dem öffentlichen Feld Kontostand vom Typ Integer (Bild 1) © Autor
Da die Methode zwei Mal aufgerufen wird, müsste der Kontostand, nachdem beide Aufrufe abgearbeitet sind, auf zwei Millionen ansteigen. So weit die in einer idealen Welt erdachte Theorie.Startet man die Beispielanwendung mehrfach, kommt es bei fast jedem Durchlauf zu einem anderen Ergebnis. Bild 2 zeigt ein Beispiel eines Durchlaufs. Die erwarteten zwei Millionen sind nicht dabei. Im Gegenteil: Die Ergebnisse sehen so aus, als würde die Anwendung bei ihrer Ausführung mit einem Würfel hantieren.
Unerwartete und unterschiedliche Ergebnisse bei jedem Programmdurchlauf (Bild 2) © Autor
Der Grund für dieses Verhalten liegt darin, dass beide Threads ohne jede Synchronisa­tion dasselbe Datenfeld ändern. Das Inkrementieren eines Integer-Wertes (kontostand++) lässt sich (etwas vereinfacht) herunterbrechen auf die folgenden Operationen:
  • Der aktuelle Wert wird ausgelesen.
  • Der Wert wird erhöht.
  • Der neue Wert wird in den Speicherbereich zurückgeschrieben.
Dadurch, dass wir bei Threading nie wissen, welcher Thread am Zug ist, und es auch nicht sicher ist, dass die beiden überhaupt gleichzeitig laufen, kann es zu Unterbrechungen in der Abarbeitung kommen. Einem Thread kann CPU-Zeit zugeordnet werden, er kann aber auch kurzzeitig angehalten werden. Dies führt dazu, dass keine Operation atomar ist und es im Beispiel zu unerklärlichen Ergebnissen kommen kann. Das Gemeine daran: Niemand warnt den Entwickler davor. Er muss solche Pro­bleme selbst erkennen und beheben.

Lösungen

Je nach Problemstellung gibt es für solche Szenarien unterschiedliche Lösungsansätze. Und für manche Spezialfälle gibt es sogar sehr einfache Lösungen. Für ein Integer-Feld wie in der Beispielklasse hat Microsoft die Klasse Interlocked geschaffen, welche das Inkrementieren des Integers atomar ausführt. Bereits eine kleine Anpassung des Codes der Klasse führt sofort zur optimalen Lösung. Bild 3 zeigt die zusätzliche Zeile Interlocked.Increment(ref kontostand), die dazu führt, dass das Problem nicht mehr auftritt. In dieser Programmfassung wird der Kontostand korrekt berechnet und erreicht die erwartete Zielsumme von 2 Millionen.
Interlocked behebt das Problem für Integer-Werte und liefert das erwartete Ergebnis (Bild 3) © Autor
Doch so schön diese Lösung auch sein mag, sie garantiert nur die „korrekte“ Abarbeitung. Es ist hier durchaus fraglich, ob eine Parallelisierung mit mehreren Threads für eine schnellere Abarbeitung sorgt, da sich in diesem Spiel zwei Threads um diesen Integer streiten (Thema: Parallelisierung ist nicht in jedem Fall sinnvoll).Außerdem: Für Integer-Felder ist die Klasse Interlocked die perfekte Lösung, allerdings hilft sie nicht weiter, wenn der Entwickler es mit komplexeren Datentypen oder eigenen Klassen zu tun hat.

Der Hammer: lock

Ein universeller „Hammer“, wie ich es gerne umschreibe, ist das Schlüsselwort lock in C#. Es erlaubt unabhängig vom Datentyp, dass der Entwickler einen sogenannten Sperrbereich definiert, in den jeweils nur ein Thread „eintreten“ und da­rin seinen Code ausführen darf.Die grundsätzliche Verwendung ist hierbei, dass mittels des Schlüsselworts lock ein .NET-Objekt als Schlüsselobjekt ausgewählt und dieses abgesperrt wird. Ein zweiter Thread kann den gesperrten Bereich so lange nicht betreten, bis dieser wieder freigegeben wird. In Bild 4 sehen Sie den entsprechend modifizierten Code der Beispielanwendung. Ein Programmlauf beweist auch hier wieder, dass damit der ­korrekte Kontostand ermittelt wird.
Lock sperrt den Zugriff auf den Kontostand und führt ebenfalls zum richtigen Endergebnis (Bild 4) © Autor
Aufgrund des Locks können wir Entwickler nun innerhalb des Sperrbereichs mit den Daten arbeiten, wie wir wollen, da garantiert wird, dass der aktuelle Thread der einzige ist, welcher den Bereich betreten darf. Sollte ein weiterer Thread den Sperrbereich betreten wollen, so wird dieser in der Zeile des lock festhängen, bis die Sperre freigegeben wurde.Die Common Language Runtime (CLR) von .NET sorgt dafür, dass ein Lock immer speziell für einen Thread gilt, weshalb auch ein doppeltes Versperren möglich ist (Bild 5). Das ist übrigens auch der Grund dafür, dass sich async und await mit lock nicht gut vertragen, da nach einem await eventuell ein Threadwechsel stattfindet und somit ein anderer ­Thread das gesperrte Objekt wieder freigibt, das er gar nicht hat. Damit das nicht passiert, quittiert der Compiler den Versuch, lock mit async und await zu kombinieren, ebenfalls mit einer Fehlermeldung.
Ein doppelter Lock (Bild 5) © Autor

Hinter den Kulissen

Spannend ist, was konkret passiert, wenn man den Lock-Befehl benutzt. Der Compiler setzt den Lock (bisher) immer mit der Klasse System.Threading.Monitor um. Diese beherbergt die Magie, um den Sperrbereich korrekt zu realisieren. In einem try/finally-Block wird versucht, diese Sperre über die Monitor-Klasse abzubilden und beim Verlassen wieder freizugeben, siehe Bild 6. Unter der Haube wird dabei im Fall von Windows die Funktion EnterCriticalSection aus dem Win32-API [3] genutzt, wie der Code-Schnipsel in Bild 7 zeigt. Das bedeutet: Eigentlich liefert das Betriebssystem die für einen Lock erforderliche Funktion.
Die Klasse System.Threading.Monitor setzt den Lock um ... (Bild 6) © Autor
... verwendet wird dabei die Funktion EnterCriticalSection aus dem Win32-API (Bild 7) © Autor

C# 13 ändert etwas …

Mit C# 13 ändert sich genau in diesem Zusammenhang nun etwas. Microsoft hat einen neuen Datentyp namens Lock eingeführt. Einerseits soll er die Lesbarkeit verbessern, da gerade die Signatur unseres Feldes object syncobject eine sehr übliche Form ist, aber es purzelt ein einfaches System.Object einfach so herum, was sich 2024 doch etwas „falsch“ anfühlt. Mit dem Datentyp Lock kann einerseits ganz klar ausgedrückt werden, dass es sich hier um ein Lock-Objekt handelt. Der betreffende Code im Beispiel sieht damit dann so aus:

private Lock _syncObject = new Lock(); 
Andererseits ist es nun nicht mehr zwangsweise so, dass der Compiler immer die Monitor-Klasse verwendet, sondern er kann mit C# in Version 13 auch auf alternative Wege zurückgreifen.Sieht man sich den erzeugten IL-Code an, so erkennt man, dass im Fall von .NET 9 und dem neuen Datentyp Lock die Methode EnterScope verwendet wird und das Objekt am Ende in einem try-finally-Block mit Dispose wieder freigegeben wird (Bild 8). Die neue Lock-Klasse stützt sich dabei also nicht mehr auf die Monitor-Klasse (die das Win32-API nutzt), sondern verschiebt die Sperrlogik in den .NET-Bereich – weg vom Betriebssystem.
Ein Objekt sperren mit C#13 und der neuen Lock-Klasse (Bild 8) © Autor

Fazit

Locking ist ein Thema, welches bei Multithreading immer aufkommt, und das Thema ist nicht nur alt, sondern auch unangenehm komplex. Aus diesem Grund wurde hier noch einmal klargestellt, warum Locking ein wichtiges Thema für Entwickler ist, und vor allem, dass Microsoft hier eine Änderung in C# 13 einführt.

Fussnoten

  1. Multithreading, http://www.dotnetpro.de/SL2411NETirol1
  2. Lock, http://www.dotnetpro.de/SL2411NETirol2
  3. Die Funktion EnterCriticalSection aus dem Win32-API, http://www.dotnetpro.de/SL2411NETirol3

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Bausteine guter Architektur - Entwurf und Entwicklung wartbarer Softwaresysteme, Teil 2
Code sauberer gestalten anhand von wenigen Patterns und Grundhaltungen.
6 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige