La instrucción lock: asegúrese del acceso exclusivo a un recurso compartido
La instrucción lock
adquiere el bloqueo de exclusión mutua de un objeto determinado, ejecuta un bloque de instrucciones y luego libera el bloqueo. Mientras se mantiene un bloqueo, el subproceso que lo mantiene puede volver a adquirir y liberar el bloqueo. Ningún otro subproceso puede adquirir el bloqueo y espera hasta que se libera. La instrucción lock
garantiza que, como máximo, un subproceso ejecute su cuerpo en cualquier momento.
La instrucción lock
tiene el formato siguiente:
lock (x)
{
// Your code...
}
La variable x
es una expresión de tipo System.Threading.Lock o un tipo de referencia. Cuando se sabe que x
en tiempo de compilación es del tipo System.Threading.Lock, es exactamente equivalente a:
using (x.EnterScope())
{
// Your code...
}
El objeto devuelto por Lock.EnterScope() es un ref struct
que incluye un método Dispose()
. La instrucción using
generada garantiza que el ámbito se libere incluso si se produce una excepción con el cuerpo de la instrucción lock
.
De lo contrario, la instrucción lock
es exactamente equivalente a:
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
Puesto que el código usa una instrucción try-finally
, el bloqueo se libera aunque se produzca una excepción dentro del cuerpo de una instrucción lock
.
No se puede usar la expresión await
en el cuerpo de una instrucción lock
.
Instrucciones
A partir de .NET 9 y C# 13, bloquee una instancia de objeto dedicada del tipo System.Threading.Lock para obtener el mejor rendimiento. Además, el compilador emite una advertencia si un objeto de Lock
conocido se convierte a otro tipo y está bloqueado. Si usa una versión anterior de .NET y C#, bloquee una instancia de objeto dedicada que no se use para otro propósito. Evite el uso de la misma instancia de objeto de bloqueo para distintos recursos compartidos, ya que se podría producir un interbloqueo o una contención de bloqueo. En particular, evite utilizar las siguientes instancias como objetos de bloqueo:
this
, ya que los autores de llamadas también pueden bloquearthis
.- Instancias Type, porque el operador o la reflexión typeof podrían obtenerlas.
- Instancias de cadena, incluidos literales de cadena, porque podrían internarse.
Mantenga el bloqueo durante el menor tiempo posible para reducir la contención de bloqueo.
Ejemplo
En el ejemplo siguiente se define una clase Account
que sincroniza el acceso a su campo privado balance
mediante el bloqueo de una instancia dedicada balanceLock
. El uso de la misma instancia para bloquear garantiza que dos subprocesos diferentes no puedan actualizar el campo de balance
llamando a los métodos Debit
o Credit
simultáneamente. En el ejemplo se usa C# 13 y el nuevo objeto Lock
. Si usa una versión anterior de C# o una biblioteca de .NET anterior, bloquee una instancia de 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));
}
}
}
}
Especificación del lenguaje C#
Para más información, consulte la sección sobre la instrucción lock de la especificación del lenguaje C#.