Что такое lock в C# и для чего он используется?

«Что такое lock в C# и для чего он используется?» — вопрос из категории Многопоточность, который задают на 51% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Ключевое слово lock в C# — это простейший механизм синхронизации, обеспечивающий взаимное исключение (mutual exclusion) для критических секций кода в многопоточных приложениях. Он гарантирует, что только один поток в данный момент может выполнять блок кода, защищенный lock для конкретного объекта-блокировки.

Основная цель: предотвращение состояний гонки (race conditions), когда несколько потоков одновременно изменяют общий ресурс, приводя к недетерминированному и ошибочному поведению.

Синтаксис и пример:

private static readonly object _syncRoot = new object(); // Объект-блокировка
private static int _sharedCounter = 0;

public void IncrementSafely()
{
    // Критическая секция. Только один поток за раз может войти сюда,
    // используя _syncRoot в качестве ключа.
    lock (_syncRoot)
    {
        _sharedCounter++; // Операция теперь атомарна для потоков
    }
}

Важные правила и лучшие практики:

  1. Выбор объекта для блокировки:

    • Используйте отдельный, приватный, readonly объект специально созданный для этой цели (обычно object).
    • НЕ блокируйте:
      • this — внешний код может случайно использовать ваш экземпляр для блокировки, что приведет к deadlock.
      • typeof(MyClass) или публичные строки — это глобальные объекты, блокировка которых может непреднамеренно синхронизировать несвязанные части приложения.
      • string — из-за интернирования строк.
  2. Краткость критической секции:

    • Код внутри lock должен выполняться как можно быстрее. Никогда не выполняйте долгие операции (ввод-вывод, сетевые запросы) внутри блокировки.
  3. Альтернативы для сложных сценариев:

    • Monitor.TryEnter(object, TimeSpan) — для блокировки с таймаутом.
    • SemaphoreSlim, Mutex, ReaderWriterLockSlim — для более сложных схем синхронизации (например, множественное чтение / эксклюзивная запись).
    • Interlocked класс — для атомарных операций над простыми типами (инкремент, сравнение с обменом).
  4. Остерегайтесь взаимной блокировки (deadlock):

    • Deadlock возникает, когда два или более потока бесконечно ждут друг друга, каждый удерживая блокировку, нужную другому.
    • Стратегия предотвращения: всегда устанавливайте строгий и одинаковый порядок захвата нескольких блокировок во всем коде.

Что происходит под капотом? Конструкция lock является синтаксическим сахаром для использования класса Monitor:

// Код: lock(_syncRoot) { _sharedCounter++; }
// Примерно эквивалентен:
object __lockObj = _syncRoot;
bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
    _sharedCounter++;
}
finally
{
    if (__lockWasTaken)
        System.Threading.Monitor.Exit(__lockObj);
}