Compartir a través de


Instrucción lock: garantizar el acceso exclusivo a un recurso compartido

La lock instrucción adquiere el bloqueo de exclusión mutua para un objeto determinado, ejecuta un bloque de instrucciones y, a continuación, libera el bloqueo. Mientras se mantiene un bloqueo, el subproceso que contiene el bloqueo puede adquirir y liberar el bloqueo varias veces. Cualquier otro subproceso está bloqueado para adquirir el bloqueo y espera hasta que se libere el bloqueo. La lock instrucción garantiza que, como máximo, un subproceso ejecute su cuerpo en cualquier momento en el tiempo.

La referencia del lenguaje C# documenta la versión publicada más recientemente del lenguaje C#. También contiene documentación inicial sobre las características de las versiones preliminares públicas de la próxima versión del lenguaje.

La documentación identifica cualquier característica introducida por primera vez en las últimas tres versiones del idioma o en las versiones preliminares públicas actuales.

Sugerencia

Para buscar cuándo se introdujo por primera vez una característica en C#, consulte el artículo sobre el historial de versiones del lenguaje C#.

La lock instrucción tiene la siguiente forma:

lock (x)
{
    // Your code...
}

La variable x es una expresión de tipo o un System.Threading.Lock referencia. Cuando el compilador sabe que x 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 objeto que incluye un Dispose() método . La instrucción generada using garantiza que el ámbito se libere incluso si se produce una excepción dentro del cuerpo de la lock instrucción.

De lo contrario, la lock instrucción 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);
}

Dado que el código usa una try-finally instrucción , el bloqueo se libera incluso si se produce una excepción dentro del cuerpo de una lock instrucción .

No se puede usar la await expresión en el cuerpo de una lock instrucción .

Directrices

A partir de .NET 9 y C# 13, bloquee una instancia de objeto dedicada del System.Threading.Lock tipo para obtener el mejor rendimiento. El compilador también emite una advertencia si convierte un objeto conocido Lock en otro tipo y lo bloquea. 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 usar la misma instancia de objeto de bloqueo para distintos recursos compartidos, ya que podría provocar interbloqueo o contención de bloqueo. En concreto, evite usar las siguientes instancias como objetos de bloqueo:

  • this, ya que los autores de llamadas también pueden bloquear this.
  • Type instancias, ya que pueden obtenerse mediante el operador typeof o la reflexión.
  • instancias de cadena, incluidos los literales de cadena, ya que se pueden internar.

Mantenga un bloqueo durante el menor tiempo posible para reducir la contención de bloqueos.

Ejemplo

En el ejemplo siguiente se define una Account clase que sincroniza el acceso a su campo privado balance bloqueando una instancia dedicada balanceLock . El uso de la misma instancia para el bloqueo garantiza que dos subprocesos diferentes no puedan actualizar el balance campo llamando a los Debit métodos o Credit simultáneamente. En el ejemplo se usa C# 13 y el nuevo Lock objeto . 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 obtener más información, vea la sección Instrucción lock de la especificación del lenguaje C#.

Consulte también