Compartir vía


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 bloquear this.
  • 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#.

Consulte también