Condividi tramite


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 assume il formato seguente:

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

La variabile x è un'espressione di tipo System.Threading.Lock o un tipo riferimento. Quando x è noto in fase di compilazione per essere del tipo System.Threading.Lock, è esattamente equivalente a:

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

L'oggetto restituito da Lock.EnterScope() è un ref struct che include un metodo Dispose(). L'istruzione using generata garantisce che l'ambito venga rilasciato anche se viene generata un'eccezione con il corpo dell'istruzione lock.

In caso contrario, l'istruzione lock è 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

A partire da .NET 9 e C# 13, bloccare un'istanza di oggetto dedicata del tipo System.Threading.Lock per ottenere prestazioni ottimali. Inoltre, il compilatore genera un avviso se viene eseguito il cast di un oggetto Lock noto a un altro tipo e bloccato. Se si usa una versione precedente di .NET e C#, bloccare in un'istanza di oggetto dedicata che non sia usata per altri scopi. 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, poiché i chiamanti potrebbero anche bloccare this.
  • 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 due thread diversi non possano aggiornare il campo balance chiamando contemporaneamente i metodi Debit o Credit. L'esempio usa C# 13 e il nuovo oggetto Lock. Se si usa una versione precedente di C# o una libreria .NET precedente, bloccare un'istanza di 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));
            }
        }
    }
}

Specifiche del linguaggio C#

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

Vedi anche