Instrukcja lock — zapewnianie wyłącznego dostępu do udostępnionego zasobu

Instrukcja lock uzyskuje blokadę wzajemnego wykluczania dla danego obiektu, wykonuje blok instrukcji, a następnie zwalnia blokadę. Gdy blokada jest utrzymywana, wątek, który przechowuje blokadę, może ponownie uzyskać i zwolnić blokadę. Każdy inny wątek nie może nabyć blokady i czeka na zwolnienie blokady. Instrukcja lock zapewnia, że w dowolnym momencie tylko jeden wątek wykonuje treść.

Instrukcja lock ma następującą formę:

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

Zmienna x jest wyrażeniem System.Threading.Lock typu lub typem odwołania. Gdy x w czasie kompilacji jest znany jako typ System.Threading.Lock, jest dokładnie odpowiednikiem:

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

Obiekt zwracany przez Lock.EnterScope() element jest obiektem ref struct zawierającym metodę Dispose() . Wygenerowana using instrukcja gwarantuje, że zakres zostanie zwolniony, nawet jeśli zostanie zgłoszony wyjątek z treścią lock instrukcji .

lock W przeciwnym razie instrukcja jest dokładnie równoważna:

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

Ponieważ kod używa try-finally instrukcji, blokada jest zwalniana, nawet jeśli wyjątek jest zgłaszany w treści instrukcji lock .

Nie można użyć await wyrażenia w treści instrukcji lock .

Wytyczne

Począwszy od platform .NET 9 i C# 13, zablokuj dedykowane wystąpienie System.Threading.Lock obiektu typu, aby uzyskać najlepszą wydajność. Ponadto kompilator wystawia ostrzeżenie, jeśli znany Lock obiekt jest rzutowany na inny typ i zablokowany. Jeśli używasz starszej wersji platformy .NET i języka C#, zablokuj wystąpienie dedykowanego obiektu, które nie jest używane w innym celu. Unikaj używania tego samego wystąpienia obiektu blokady dla różnych zasobów udostępnionych, ponieważ może to spowodować zakleszczenie lub zablokowanie rywalizacji. W szczególności należy unikać używania następujących wystąpień jako obiektów blokady:

  • this, ponieważ osoby wywołujące mogą również zablokować thisfunkcję .
  • Type wystąpienia, ponieważ mogą być uzyskiwane przez operator typeof lub odbicie.
  • wystąpienia ciągów, w tym literały ciągów, ponieważ mogą być internowane.

Przytrzymaj blokadę tak szybko, jak to możliwe, aby zmniejszyć rywalizację o blokadę.

Przykład

W poniższym przykładzie zdefiniowano klasę Account , która synchronizuje dostęp z polem prywatnym balance przez zablokowanie wystąpienia dedykowanego balanceLock . Użycie tego samego wystąpienia do blokowania gwarantuje, że dwa różne wątki nie mogą zaktualizować balance pola przez wywołanie Debit metod lub Credit jednocześnie. W przykładzie użyto języka C# 13 i nowego Lock obiektu. Jeśli używasz starszej wersji języka C# lub starszej biblioteki .NET, zablokuj wystąpienie programu 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));
            }
        }
    }
}

specyfikacja języka C#

Aby uzyskać więcej informacji, zobacz sekcję instrukcji lock specyfikacji języka C#.

Zobacz też