istruzione lock: garantire l'accesso esclusivo a una risorsa condivisa

L'istruzione lock acquisisce il blocco a esclusione reciproca per un oggetto specificato, esegue un blocco di istruzioni e quindi rilascia il blocco. Mentre è attivo un blocco, il thread che contiene il blocco può ancora acquisire e rilasciare il blocco. Gli altri thread non possono acquisire il blocco e devono attendere finché il blocco non viene rilasciato. L'istruzione lock garantisce che al massimo un solo thread esegua il corpo in qualsiasi momento.

L'istruzione lock ha il formato

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

in cui x è un'espressione di un tipo riferimento. È esattamente 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);
}

Poiché il codice usa un'try-finally istruzione, il blocco viene rilasciato anche se viene generata un'eccezione all'interno del corpo di un'istruzione lock.

Non è possibile usare l'awaitespressione nel corpo di un'istruzione lock.

Linee guida

Quando si sincronizza l'accesso dei thread a una risorsa condivisa, applicare il blocco a un'istanza dell'oggetto dedicata (ad esempio private readonly object balanceLock = new object();) o a un'altra istanza che ha poche probabilità di essere usata come oggetto di blocco da parti del codice non correlate. Evitare di usare la stessa istanza di oggetto di blocco per diverse risorse condivise. Questo può originare problemi di deadlock o conflitti di blocco. In particolare, evitare di usare gli elementi seguenti come oggetti di blocco:

  • this, perché potrebbe essere usato dai chiamanti come blocco.
  • istanze Type, in quanto possono essere ottenute dall'operatore typeof o dalla reflection.
  • istanze stringa, tra cui i valori letterali stringa, in quanto potrebbero essere centralizzate.

Tenere premuto un blocco per il minor tempo possibile per ridurre la contesa di blocco.

Esempio

L'esempio seguente definisce una classe Account che sincronizza l'accesso al proprio campo balance privato applicando il blocco su un'istanza balanceLock dedicata. L'uso della stessa istanza per il blocco garantisce che il campo balance non possa essere aggiornato contemporaneamente da due thread che provano a chiamare i metodi Debit o Credit contemporaneamente.

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));
            }
        }
    }
}

Specifiche del linguaggio C#

Per altre informazioni, vedere la sezione Istruzione lock della specifica del linguaggio C#.

Vedi anche