Instrução lock: garantir o acesso exclusivo a um recurso compartilhado

A instrução lock obtém o bloqueio de exclusão mútua para um determinado objeto, executa um bloco de instruções e, em seguida, libera o bloqueio. Embora um bloqueio seja mantido, o thread que mantém o bloqueio pode adquiri-lo novamente e liberá-lo. Qualquer outro thread é impedido de adquirir o bloqueio e aguarda até que ele seja liberado. A instrução lock garante que, no máximo, apenas um thread execute seu corpo a qualquer momento.

O argumento lock usa o seguinte formato:

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

A variável x é uma expressão do tipo System.Threading.Lock, ou um tipo de referência. Quando x é conhecido em tempo de compilação como sendo do tipo System.Threading.Lock, é exatamente equivalente a:

using (x.EnterScope())
{
    // Your code...
}

O objeto retornado por Lock.EnterScope() é um ref struct que inclui um método Dispose(). A instrução using gerada garante que o escopo será liberado mesmo se uma exceção for lançada dentro do corpo da instrução lock.

Caso contrário, a instrução lock é exatamente 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);
}

Como o código usa uma instrução try-finally, o bloqueio será liberado mesmo se uma exceção for gerada dentro do corpo de uma instrução lock.

Não é possível usar a expressão await no corpo de uma instrução lock.

Diretrizes

A partir do .NET 9 e C# 13, bloqueie uma instância de objeto dedicada do tipo System.Threading.Lock para atingir o melhor desempenho. Além disso, o compilador emitirá um aviso se um objeto Lock conhecido for convertido para outro tipo e depois bloqueado. Se estiver usando uma versão mais antiga do .NET e do C#, bloqueie uma instância de objeto dedicada que não esteja sendo usada para outra finalidade. Evite usar a mesma instância de objeto de bloqueio para diferentes recursos compartilhados, uma vez que ela poderia resultar em deadlock ou contenção de bloqueio. Especificamente, evite usar as seguintes instâncias como objetos de bloqueio:

  • this, já que outros chamadores também podem bloquear this.
  • Instâncias Type, pois elas podem ser obtidas pelo operador ou reflexão typeof.
  • Instâncias de cadeia de caracteres, incluindo literais de cadeia de caracteres, pois podem ser internalizadas.

Mantenha um bloqueio pelo menor tempo possível para reduzir a contenção de bloqueio.

Exemplo

O exemplo a seguir define uma classe Account que sincroniza o acesso com seu campo privado balance bloqueando uma instância balanceLock dedicada. Usar a mesma instância para bloquear garante que dois threads diferentes não possam atualizar o campo balance chamando os métodos Debit ou Credit simultaneamente. O exemplo usa C# 13 e o novo objeto Lock. Se você estiver usando uma versão mais antiga do C# ou uma biblioteca .NET mais antiga, bloqueie uma instância 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));
            }
        }
    }
}

Especificação da linguagem C#

Para saber mais, confira a seção A instrução lock na especificação da linguagem C#.

Confira também