Поделиться через


Инструкция блокировки — обеспечение монопольного доступа к общему ресурсу

Оператор lock получает взаимоисключающую блокировку заданного объекта перед выполнением определенных операторов, а затем снимает блокировку. Во время блокировки поток, удерживающий блокировку, может снова поставить и снять блокировку. Любой другой поток не может получить блокировку и ожидает ее снятия. Оператор lock гарантирует, что в любой момент выполняется только один поток.

Оператор lock принимает следующую форму:

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

Переменная x — это выражение System.Threading.Lock типа или ссылочного типа. Когда x известно во время System.Threading.Lockкомпиляции тип, он точно эквивалентен следующим:

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

Объект, возвращаемый Lock.EnterScope() методомDispose(), является ref struct объектом, который включает метод. Созданная using инструкция гарантирует, что область освобождается, даже если исключение создается с текстом инструкцииlock.

lock В противном случае оператор точно эквивалентен следующему:

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

Так как код использует try-finally инструкцию, блокировка освобождается даже в том случае, если исключение создается в тексте lock инструкции.

Выражение в тексте инструкции нельзя использоватьawait.lock

Рекомендации

Начиная с .NET 9 и C# 13, блокировка выделенного экземпляра объекта типа для оптимальной System.Threading.Lock производительности. Кроме того, компилятор выдает предупреждение, если известный Lock объект приведение к другому типу и заблокирован. Если используется более ранняя версия .NET и C#, блокировка на выделенном экземпляре объекта, который не используется для другой цели. Не используйте один и тот же экземпляр объекта блокировки для разных общих ресурсов: это может привести к взаимоблокировке или состязанию при блокировке. В частности, избегайте использования следующих экземпляров в качестве объектов блокировки:

  • this, как вызывающие также могут блокироваться this.
  • Typeэкземпляры, так как они могут быть получены оператором typeof или отражением.
  • экземпляры строк, включая строковые литералы, так как они могут быть интернированы.

Удерживайте блокировку в течение максимально короткого времени, чтобы сократить число конфликтов при блокировке.

Пример

В следующем примере определяется класс Account, который синхронизирует доступ к закрытому полю balance путем блокировки выделенного экземпляра balanceLock. Использование одного и того же экземпляра для блокировки гарантирует, что два разных потока не могут обновлять balance поле, вызывая Debit методы или Credit методы одновременно. В примере используется C# 13 и новый Lock объект. Если вы используете более раннюю версию C# или более старую библиотеку .NET, заблокируйте экземпляр 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));
            }
        }
    }
}

Спецификация языка C#

Дополнительные сведения см. в разделе об инструкции lock в документации Предварительная спецификация C# 6.0.

См. также