Partager via


Instruction lock : garantir un accès exclusif à une ressource partagée

L’instruction lock acquiert le verrou d’exclusion mutuelle pour 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 est bloqué pour acquérir le verrou et attend que le verrou 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 System.Threading.Lock type ou un type référence. Lorsqu’il x est connu au moment de la compilation pour être du type System.Threading.Lock, il est précisément équivalent à :

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

L’objet retourné par Lock.EnterScope() est un ref struct objet qui inclut une Dispose() méthode. L’instruction générée using garantit que l’étendue est libérée même si une exception est levée avec le corps de l’instruction lock .

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

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

Étant donné que le code utilise une try-finally instruction, le verrou est libéré même si une exception est levée dans le corps d’une lock instruction.

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

Lignes directrices

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

  • this, car les appelants peuvent également verrouiller this.
  • Type instances, telles qu’elles peuvent être obtenues par l’opérateur typeof ou la réflexion.
  • instances de chaîne, y compris les littéraux de chaîne, car elles peuvent être internes.

Maintenez un verrou aussi court que possible pour réduire la contention de verrou.

Exemple :

L’exemple suivant définit une Account classe qui synchronise l’accès à son champ privé balance en verrouillant sur une instance dédiée balanceLock . L’utilisation de la même instance pour le verrouillage garantit que deux threads différents ne peuvent pas mettre à jour le balance champ en appelant simultanément les méthodes ou Credit les Debit méthodes. L’exemple utilise C# 13 et le nouvel Lock objet. Si vous utilisez une version antérieure de C# ou une bibliothèque .NET plus ancienne, 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, consultez la section De l’instruction lock de la spécification du langage C#.

Voir aussi