De vergrendelingsinstructie: exclusieve toegang tot een gedeelde resource garanderen
De lock
instructie verkrijgt de wederzijdse uitsluitingsvergrendeling voor een bepaald object, voert een instructieblok uit en geeft vervolgens de vergrendeling vrij. Terwijl een vergrendeling wordt vastgehouden, kan de thread die de vergrendeling bevat, de vergrendeling opnieuw verkrijgen en vrijgeven. Elke andere thread wordt geblokkeerd voor het verkrijgen van de vergrendeling en wacht totdat de vergrendeling wordt vrijgegeven. De lock
instructie zorgt ervoor dat maximaal één thread de hoofdtekst op elk moment uitvoert.
De lock
instructie heeft de volgende vorm:
lock (x)
{
// Your code...
}
De variabele x
is een expressie van System.Threading.Lock het type of een verwijzingstype. Wanneer x
wordt bekend dat het type compileertijd System.Threading.Lockheeft, is het precies gelijk aan:
using (x.EnterScope())
{
// Your code...
}
Het object dat wordt geretourneerd door Lock.EnterScope() is een ref struct
methode die een Dispose()
methode bevat. De gegenereerde using
instructie zorgt ervoor dat het bereik wordt vrijgegeven, zelfs als er een uitzondering wordt gegenereerd met de hoofdtekst van de lock
instructie.
Anders is de lock
instructie precies gelijk aan:
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
Omdat de code gebruikmaakt van een try-finally
instructie, wordt de vergrendeling vrijgegeven, zelfs als er een uitzondering wordt gegenereerd in de hoofdtekst van een lock
instructie.
U kunt de await
expressie niet gebruiken in de hoofdtekst van een lock
instructie.
Richtlijnen
Vanaf .NET 9 en C# 13 vergrendelt u een toegewezen objectexemplaren van het type voor de System.Threading.Lock beste prestaties. Bovendien geeft de compiler een waarschuwing uit als een bekend Lock
object naar een ander type wordt gecast en vergrendeld. Als u een oudere versie van .NET en C# gebruikt, vergrendelt u een toegewezen objectexemplaren die niet voor een ander doel worden gebruikt. Vermijd het gebruik van hetzelfde exemplaar van het vergrendelingsobject voor verschillende gedeelde resources, omdat dit kan leiden tot impasse- of vergrendelingsconflicten. Vermijd met name het gebruik van de volgende exemplaren als vergrendelingsobjecten:
this
, omdat bellers ook kunnen vergrendelenthis
.- Type exemplaren, zoals ze kunnen worden verkregen door de operator of weerspiegeling van het type .
- tekenreeksexemplaren, inclusief letterlijke tekenreeksen, omdat ze mogelijk worden geïnterneerd.
Houd een vergrendeling zo kort mogelijk vast om conflicten met de vergrendeling te verminderen.
Opmerking
In het volgende voorbeeld wordt een Account
klasse gedefinieerd waarmee de toegang tot het privéveld balance
wordt gesynchroniseerd door een toegewezen balanceLock
exemplaar te vergrendelen. Als u hetzelfde exemplaar gebruikt voor het vergrendelen, zorgt u ervoor dat twee verschillende threads het veld niet kunnen bijwerken door het balance
Debit
of Credit
de methoden tegelijkertijd aan te roepen. In het voorbeeld wordt C# 13 en het nieuwe Lock
object gebruikt. Als u een oudere versie van C# of een oudere .NET-bibliotheek gebruikt, vergrendelt u een exemplaar van object
.
using System;
using System.Threading.Tasks;
public class Account
{
// Use `object` in versions earlier than C# 13
private readonly System.Threading.Lock _balanceLock = new();
private decimal _balance;
public Account(decimal initialBalance) => _balance = initialBalance;
public decimal Debit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative.");
}
decimal appliedAmount = 0;
lock (_balanceLock)
{
if (_balance >= amount)
{
_balance -= amount;
appliedAmount = amount;
}
}
return appliedAmount;
}
public void Credit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative.");
}
lock (_balanceLock)
{
_balance += amount;
}
}
public decimal GetBalance()
{
lock (_balanceLock)
{
return _balance;
}
}
}
class AccountTest
{
static async Task Main()
{
var account = new Account(1000);
var tasks = new Task[100];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(() => Update(account));
}
await Task.WhenAll(tasks);
Console.WriteLine($"Account's balance is {account.GetBalance()}");
// Output:
// Account's balance is 2000
}
static void Update(Account account)
{
decimal[] amounts = [0, 2, -3, 6, -2, -1, 8, -5, 11, -6];
foreach (var amount in amounts)
{
if (amount >= 0)
{
account.Credit(amount);
}
else
{
account.Debit(Math.Abs(amount));
}
}
}
}
C#-taalspecificatie
Zie de sectie Vergrendelingsinstructie van de C#-taalspecificatie voor meer informatie.