Как работает lock? Можно ли использовать структуры внутри выражения lock?csharp-131

1. Как работает оператор lock в C#

1.1. Базовый механизм

Оператор lock обеспечивает взаимное исключение (mutual exclusion) для критических секций кода:

private readonly object _lockObj = new object();

void ThreadSafeMethod()
{
    lock (_lockObj)
    {
        // Критическая секция
    }
}

Что происходит при выполнении:

  1. CLR использует монитор (System.Threading.Monitor) для синхронизации
  2. При входе в блок: Monitor.Enter(_lockObj)
  3. При выходе (даже при исключении): Monitor.Exit(_lockObj)

1.2. Подробности реализации

Компилятор преобразует lock в следующий код:

object __lockObj = _lockObj;
bool __lockTaken = false;
try
{
    Monitor.Enter(__lockObj, ref __lockTaken);
    // Критическая секция
}
finally
{
    if (__lockTaken)
        Monitor.Exit(__lockObj);
}

2. Почему нельзя использовать структуры в lock

2.1. Основная причина

Структуры являются value-типами и при передаче в Monitor.Enter:

  • Упаковываются (boxing) в новый объект
  • Каждый вызов создает новый экземпляр объекта для блокировки
  • Потеря синхронизации между потоками

2.2. Демонстрация проблемы

struct LockStruct { }

LockStruct lockStruct = new LockStruct();

// Поток 1:
lock (lockStruct) // Упаковка в объект A
{
    // ...
}

// Поток 2:
lock (lockStruct) // Упаковка в объект B - ДРУГОЙ объект!
{
    // Не будет ждать освобождения из потока 1
}

2.3. Ошибка компиляции

В современных версиях C# компилятор явно запрещает использование структур:

lock (new MyStruct()) // Ошибка CS0185
{
}

CS0185: "Тип 'MyStruct' не может быть использован как аргумент для монитора, так как не является ссылочным типом"

3. Альтернативы для value-типов

3.1. Обертка в object

private readonly object _lockObj = new object();

lock (_lockObj) // Корректно
{
}

3.2. Использование Mutex/Semaphore

Для сложных сценариев:

private readonly Mutex _mutex = new Mutex();

_mutex.WaitOne();
try
{
    // Критическая секция
}
finally
{
    _mutex.ReleaseMutex();
}

3.3. Value-типы с явной упаковкой

int lockInt = 42;
object boxed = lockInt; // Явная упаковка

lock (boxed) // Технически работает, но опасно!
{
}

4. Best Practices для lock

  1. Всегда используйте private readonly object для блокировки
  2. Избегайте блокировки на:
    • this
    • Type-объекты (typeof(MyClass))
    • строки
  3. Минимизируйте время удержания блокировки
  4. Используйте Monitor.TryEnter для таймаутов:
if (Monitor.TryEnter(_lockObj, TimeSpan.FromSeconds(1)))
{
    try { /* работа */ }
    finally { Monitor.Exit(_lockObj); }
}
else
{
    // Обработка таймаута
}

Резюмируем:

lock использует мониторы CLR для потокобезопасности и требует ссылочных типов, так как value-типы упаковываются в новые объекты, нарушая синхронизацию. Для структур используйте обертки в object или альтернативные механизмы синхронизации.