Что такое lock в 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);
}

Ответ 18+ 🔞

Слушай, вот объясняю про этот ваш lock в C#, как будто ты на кухне сидишь и пытаешься с братаном один бутерброд на двоих поделить. Без блокировки вы оба хватаетесь за колбасу одновременно, хрен знает, кто сколько оторвёт — это и есть состояние гонки, блядь. А lock — это как мамино "не трогай, пока брат не доест". Только один в данный момент может жрать.

Зачем это надо? Чтобы твои потоки не устроили бардак в общих переменных. Представь, два потока пытаются увеличить один счётчик. Без блокировки они могут прочитать старое значение одновременно, оба прибавят единицу и записать обратно — в итоге вместо двух прибавлений будет одно, счётчик ебётся. А с lock — один зашёл, сделал дело, вышел, потом второй.

Как выглядит эта магия?

private static readonly object _locker = new object(); // Это наш "замок", обычный болванчик
private static int _counter = 0;

public void AddOne()
{
    lock (_locker) // Ключ в руках? Заходи. Нет? Стоишь и ждёшь у параши.
    {
        _counter++; // Вот тут спокойно работаем, никто не помешает
    }
}

Главные правила, чтобы не выстрелить себе в ногу:

  1. На что вешать lock?

    • Нормально: Создай отдельный private readonly object специально для этого. Как личный шкафчик с ключом.
    • Хуёво и опасно:
      • this — а вдруг кто-то снаружи тоже решит залочить твой объект? Получится мёртвая хватка (deadlock), будете вечно ждать друг друга, ебать колотить.
      • typeof(MyClass) или публичные строки — это как вешать замок на парадную дверь подъезда. Все соседи не смогут выйти, потому что ты в своей квартире код выполняешь. Полный пиздец.
      • string — они там внутри могут быть одним и тем же объектом из-за интернирования, сюрприз, блядь!
  2. Не засиживайся! Внутри lock делай только самое необходимое и быстрое. Никаких запросов в базу, чтения файлов или долгих расчётов. Представь, ты в туалете с замком, а там решил побриться, помыться и зубки почистить. Очередь снаружи взбесится, а программа — зависнет.

  3. Есть и другие инструменты.

    • Monitor.TryEnter — это как постучаться в туалет: "Мужик, ты скоро? А то ща обоссусь!" Можно с таймаутом.
    • SemaphoreSlim — это уже как турникет в метро: пускает, скажем, только пятерых одновременно.
    • Interlocked — для простых операций (прибавить, сравнить). Это как атомарно хлопнуть дверью — быстро и без лишних церемоний.
  4. Про deadlock (взаимную блокировку). Это когда Поток 1 схватил замок А и ждёт замок Б, а Поток 2 схватил замок Б и ждёт замок А. И оба будут ждать до второго пришествия. Как не допустить? Договорись, блядь, на уровне всего кода всегда брать несколько замков в одном и том же порядке. Сначала всегда А, потом Б. Тогда один будет ждать другого, но не оба одновременно.

А что там внутри? Да всё просто, lock — это просто красивый фантик для старого доброго Monitor. Компилятор разворачивает его примерно так:

// Твой красивый lock(_locker) { _counter++; }
object obj = _locker;
bool lockTaken = false;
try
{
    System.Threading.Monitor.Enter(obj, ref lockTaken); // Вошли
    _counter++;
}
finally
{
    if (lockTaken)
        System.Threading.Monitor.Exit(obj); // Выйдем в любом случае, даже если исключение
}

Короче, используй lock точечно, для маленьких кусков кода, и не выёбывайся со странными объектами для блокировки — и будет тебе счастье в многопоточном мире.