Почему нельзя использовать структуру в качестве объекта для lock в C#
Что проверяют
Понимаете ли вы, что lock работает с заголовком объекта в куче, а не со значением, и отличаете ли ошибку компиляции от молчаливой потери взаимного исключения.
Как отвечать
lock(x) разворачивается в Monitor.Enter и Monitor.Exit, а монитор живёт в заголовке объекта в куче. У значимого типа такого заголовка нет, поэтому компилятор просто не даст написать lock по переменной структуры: это ошибка сборки, а не предупреждение.
Опасно другое: эту ошибку обходят приведением к object. Код собирается, но каждое приведение упаковывает копию структуры в новый объект в куче, и у каждой копии свой монитор. Два потока заходят в блокировку по разным объектам и оба спокойно проходят внутрь, взаимного исключения нет вовсе. Отладка получается тяжёлой: гонка есть, а lock в коде стоит на месте.
Правильный вариант это выделенное приватное поле ссылочного типа:
private readonly object _gate = new object();
lock (_gate) { /* критическая секция */ }Поле приватное, чтобы снаружи никто не взял ту же блокировку, и readonly, чтобы его нельзя было переприсвоить. Если потокобезопасной должна стать сама структура, её защищают отдельным объектом, а когда речь про одно числовое поле, обходятся Interlocked.
Где спотыкаются
Называют упаковку проблемой производительности. Лишний объект в куче тут дело десятое: ломается сама блокировка, потому что упакованные копии это разные объекты с разными мониторами.
Второе: вместо структуры берут this, typeof или строковый литерал. Строки интернируются, тип и this видны снаружи, и чужой код случайно встаёт на ту же блокировку, а взаимные блокировки появляются там, где их никто не ждал.
Третье: поле блокировки объявляют обычным, без readonly, и где-то присваивают ему новый объект. После этого потоки расходятся по разным мониторам ровно так же, как при упаковке, только искать причину ещё дольше.