Ответ
Ключевое слово 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++; // Операция теперь атомарна для потоков
}
}
Важные правила и лучшие практики:
-
Выбор объекта для блокировки:
- Используйте отдельный, приватный,
readonlyобъект специально созданный для этой цели (обычноobject). - НЕ блокируйте:
this— внешний код может случайно использовать ваш экземпляр для блокировки, что приведет к deadlock.typeof(MyClass)или публичные строки — это глобальные объекты, блокировка которых может непреднамеренно синхронизировать несвязанные части приложения.string— из-за интернирования строк.
- Используйте отдельный, приватный,
-
Краткость критической секции:
- Код внутри
lockдолжен выполняться как можно быстрее. Никогда не выполняйте долгие операции (ввод-вывод, сетевые запросы) внутри блокировки.
- Код внутри
-
Альтернативы для сложных сценариев:
Monitor.TryEnter(object, TimeSpan)— для блокировки с таймаутом.SemaphoreSlim,Mutex,ReaderWriterLockSlim— для более сложных схем синхронизации (например, множественное чтение / эксклюзивная запись).Interlockedкласс — для атомарных операций над простыми типами (инкремент, сравнение с обменом).
-
Остерегайтесь взаимной блокировки (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++; // Вот тут спокойно работаем, никто не помешает
}
}
Главные правила, чтобы не выстрелить себе в ногу:
-
На что вешать lock?
- Нормально: Создай отдельный
private readonly objectспециально для этого. Как личный шкафчик с ключом. - Хуёво и опасно:
this— а вдруг кто-то снаружи тоже решит залочить твой объект? Получится мёртвая хватка (deadlock), будете вечно ждать друг друга, ебать колотить.typeof(MyClass)или публичные строки — это как вешать замок на парадную дверь подъезда. Все соседи не смогут выйти, потому что ты в своей квартире код выполняешь. Полный пиздец.string— они там внутри могут быть одним и тем же объектом из-за интернирования, сюрприз, блядь!
- Нормально: Создай отдельный
-
Не засиживайся! Внутри
lockделай только самое необходимое и быстрое. Никаких запросов в базу, чтения файлов или долгих расчётов. Представь, ты в туалете с замком, а там решил побриться, помыться и зубки почистить. Очередь снаружи взбесится, а программа — зависнет. -
Есть и другие инструменты.
Monitor.TryEnter— это как постучаться в туалет: "Мужик, ты скоро? А то ща обоссусь!" Можно с таймаутом.SemaphoreSlim— это уже как турникет в метро: пускает, скажем, только пятерых одновременно.Interlocked— для простых операций (прибавить, сравнить). Это как атомарно хлопнуть дверью — быстро и без лишних церемоний.
-
Про 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 точечно, для маленьких кусков кода, и не выёбывайся со странными объектами для блокировки — и будет тебе счастье в многопоточном мире.
Видео-ответы
▶
▶
▶
▶
▶
▶
▶