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.

A instrução lock está no formato

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

em que x é uma expressão de um tipo de referência. Ela é precisamente 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

Ao sincronizar o acesso de thread com um recurso compartilhado, bloqueie uma instância de objeto dedicada (por exemplo, private readonly object balanceLock = new object();) ou outra instância que provavelmente não será usada como um objeto de bloqueio por partes não relacionadas do código. 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, uma vez que pode ser usado pelos chamadores como um bloqueio.
  • 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 o campo balance não pode ser atualizado simultaneamente por dois threads que tentam chamar os métodos Debit ou Credit simultaneamente.

using System;
using System.Threading.Tasks;

public class Account
{
    private readonly object balanceLock = new object();
    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