Condividi tramite


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

L'istruzione lock acquisisce il blocco di esclusione reciproca per un determinato oggetto, esegue un blocco di istruzioni e quindi rilascia il blocco. Mentre si tiene un blocco, il thread che contiene il blocco può acquisire e rilasciare nuovamente il blocco. Qualsiasi altro thread viene bloccato dall'acquisizione del blocco e attende fino al rilascio del blocco. L'istruzione lock garantisce che al massimo un thread esenga il corpo in qualsiasi momento.

L'istruzione lock ha il formato seguente:

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

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

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

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

In caso contrario, l'istruzione lock equivale esattamente 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'istruzionetry-finally , il blocco viene rilasciato anche se viene generata un'eccezione all'interno del corpo di un'istruzionelock.

Non è possibile usare l'espressioneawait nel corpo di un'istruzionelock.

Istruzioni

A partire da .NET 9 e C# 13, bloccare un'istanza di oggetto dedicata del System.Threading.Lock tipo per ottenere prestazioni ottimali. Inoltre, il compilatore genera un avviso se viene eseguito il cast di un oggetto noto Lock 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 viene usata per un altro scopo. Evitare di usare la stessa istanza dell'oggetto lock per risorse condivise diverse, in quanto potrebbe verificarsi un deadlock o una contesa di blocco. In particolare, evitare di usare le istanze seguenti come oggetti di blocco:

  • this, poiché i chiamanti potrebbero anche bloccare this.
  • Type istanze, come potrebbero essere ottenute dall'operatore typeof o reflection.
  • istanze di stringa, inclusi i valori letterali stringa, in quanto potrebbero essere internati.

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

Esempio

Nell'esempio seguente viene definita una Account classe che sincronizza l'accesso al relativo campo privato balance bloccando in un'istanza dedicata balanceLock . L'uso della stessa istanza per il blocco garantisce che due thread diversi non possano aggiornare il balance campo chiamando contemporaneamente i Debit metodi o Credit . L'esempio usa C# 13 e il nuovo Lock oggetto . 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));
            }
        }
    }
}

Specificazione del linguaggio C#

Per altre informazioni, vedere La sezione relativa all'istruzione lock della specifica del linguaggio C#.

Vedere anche