Instruction Lock : veiller à un accès exclusif à une ressource partagée

L’instruction lock obtient le verrou d’exclusion mutuelle d’un objet donné, exécute un bloc d’instructions, puis libère le verrou. Tant qu’un verrou est maintenu, le thread qui contient le verrou peut à nouveau obtenir et libérer le verrou. Tout autre thread se voit bloquer l’obtention du verrou et attend que ce dernier soit libéré. L’instruction lock garantit qu’au maximum un seul thread exécute son corps à tout moment.

L’instruction lock prend la forme suivante :

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

La variable x est une expression de type System.Threading.Lock ou un type référence. Quand x est connu au moment de la compilation pour être de type System.Threading.Lock, il exactement équivalent à :

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

L’objet renvoyé par Lock.EnterScope() est un ref struct qui inclut une méthode Dispose(). L’instruction using générée veille à ce que l’étendue soit publiée, même en cas de levée d’une exception avec le corps de l’instruction lock.

Sinon, l’instruction lock est exactement égale à :

object __lockObj = x;
bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
    // Your code...
}
finally
{
    if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}

Dans la mesure où le code utilise une instruction try-finally, le verrou est libéré même si une exception est levée dans le corps d’une instruction lock.

Vous ne pouvez pas utiliser l’expression await dans le corps d’une instruction lock.

Consignes

À compter de .NET 9 et C# 13, verrouillez une instance d’objet dédiée de type System.Threading.Lock pour obtenir de meilleures performances. En outre, le compilateur émet un avertissement si un objet Lock connu est casté vers un autre type et verrouillé. Si vous utilisez une ancienne version de .NET et C#, verrouillez sur une instance d’objet dédié non utilisée à d’autres fins. Évitez d’utiliser la même instance d’objet de verrouillage pour différentes ressources partagées, car cela peut entraîner une contention d’interblocage ou de verrouillage. En particulier, évitez d’utiliser les instances suivantes en tant qu’objets de verrouillage :

  • this, car il est possible que des appelants verrouillent également this.
  • les instances Type, car elles peuvent être obtenues par l’opérateur typeof ou par réflexion.
  • les instances de chaîne, notamment les littéraux de chaîne, qui peuvent être internés.

Maintenez le verrouillage aussi peu de temps que possible pour réduire les conflits d’accès.

Exemple

L’exemple suivant définit une classe Account, qui synchronise l’accès à son champ balance privé en verrouillant une instance balanceLock dédiée. L’utilisation de la même instance pour le verrouillage veille à ce que deux threads différents ne puissent pas mettre à jour le champ balance en appelant les méthodes Debit ou Credit simultanément. L’exemple utilise C# 13 et le nouvel objet Lock. Si vous utilisez une ancienne version de C# ou une ancienne bibliothèque .NET, verrouillez une instance 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));
            }
        }
    }
}

spécification du langage C#

Pour plus d’informations, voir la section Instruction lock de la spécification du langage C#.

Voir aussi