Start Debugging

.NET 9: конец lock(object)

В .NET 9 появилась System.Threading.Lock -- выделенная лёгкая примитива синхронизации, заменяющая lock(object) лучшей производительностью и более ясным намерением.

Почти два десятилетия разработчики на C# использовали простой шаблон для синхронизации потоков: создать приватный экземпляр object и передать его инструкции lock. Этот подход работает, но скрывает издержки производительности, которые .NET 9 наконец устраняет с появлением System.Threading.Lock.

Скрытая цена Monitor

Когда вы пишете lock (myObj), компилятор транслирует это в вызовы System.Threading.Monitor.Enter и Monitor.Exit. Этот механизм опирается на object header word — кусочек метаданных, прикреплённый к каждому ссылочному типу в управляемой куче.

Использование обычного object для блокировки заставляет среду выполнения:

  1. Выделять объект в куче только ради идентичности.
  2. Расширять заголовок объекта для размещения информации о синхронизации (“sync block”) при возникновении конкуренции.
  3. Создавать давление на сборку мусора (GC), даже если объект никогда не покидает класс.

В сценариях с высокой пропускной способностью эти микровыделения и манипуляции с заголовком накапливаются.

Появляется System.Threading.Lock

.NET 9 представляет выделенный тип: System.Threading.Lock. Это не просто обёртка над Monitor; это лёгкая примитива синхронизации, спроектированная специально для взаимного исключения.

Когда компилятор C# 13 встречает инструкцию lock, нацеленную на экземпляр System.Threading.Lock, он генерирует другой код. Вместо Monitor.Enter вызывается Lock.EnterScope(), возвращающий структуру Lock.Scope. Эта структура реализует IDisposable для освобождения блокировки, обеспечивая безопасность потоков даже при возникновении исключений.

До и после

Вот традиционный подход, от которого мы уходим:

public class LegacyCache
{
    // The old way: allocating a heap object just for locking
    private readonly object _syncRoot = new();
    private int _count;

    public void Increment()
    {
        lock (_syncRoot) // Compiles to Monitor.Enter(_syncRoot)
        {
            _count++;
        }
    }
}

А вот современный шаблон в .NET 9:

using System.Threading;

public class ModernCache
{
    // The new way: a dedicated lock instance
    private readonly Lock _sync = new();
    private int _count;

    public void Increment()
    {
        // C# 13 recognizes this type and optimizes the IL
        lock (_sync) 
        {
            _count++;
        }
    }
}

Почему это важно

Улучшения структурные:

  1. Чёткое намерение: имя типа Lock явно декларирует назначение, в отличие от обобщённого object.
  2. Производительность: System.Threading.Lock избегает накладных расходов sync block в заголовке объекта. Он использует более эффективную внутреннюю реализацию, сокращающую такты CPU при захвате и освобождении блокировки.
  3. Запас на будущее: использование выделенного типа позволяет среде выполнения дальше оптимизировать механику блокировок, не ломая существующее поведение Monitor.

Лучшие практики

Эта возможность требует одновременно .NET 9 и C# 13. Если вы обновляете существующий проект, можно механически заменить private readonly object _lock = new(); на private readonly Lock _lock = new();. Остальное возьмёт на себя компилятор.

Не выставляйте экземпляр Lock наружу. Как и со старым шаблоном на основе object, инкапсуляция — ключ к предотвращению взаимных блокировок, вызванных внешним кодом, захватывающим ваши внутренние примитивы синхронизации.

Для разработчиков высоконагруженных систем эта небольшая перемена представляет значительный шаг вперёд в снижении накладных расходов среды выполнения.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад