Поделиться через


объект Lock

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую на этапах проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Различия отражены в соответствующих заметках собраний по проектированию языка (LDM) .

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/7104

Сводка

Особый случай взаимодействия System.Threading.Lock с ключевым словом lock (вызов метода EnterScope неявно). Добавьте предупреждения статического анализа, чтобы предотвратить случайное неправильное использование типа, где это возможно.

Мотивация

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

namespace System.Threading
{
    public sealed class Lock
    {
        public void Enter();
        public void Exit();
        public Scope EnterScope();
    
        public ref struct Scope
        {
            public void Dispose();
        }
    }
}

Подробный дизайн

Семантика инструкции блокировки (§13.13) изменяется, чтобы учитывать особый случай для типа System.Threading.Lock:

Инструкция lock формы lock (x) { ... }

  1. , где x является выражением типа System.Threading.Lock, точно эквивалентно:
    using (x.EnterScope())
    {
        ...
    }
    
    и System.Threading.Lock должны иметь следующую форму:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. где x — это выражение типа ссылки , и это точно эквивалентно: [...]

Обратите внимание, что форма может не быть полностью проверена (например, не будет ошибок или предупреждений, если тип Lock не является sealed), но функция может не работать так, как ожидалось (например, не будет предупреждений, когда тип Lock преобразуется в производный, так как функция предполагает отсутствие производных типов).

Кроме того, новые предупреждения добавляются в неявные преобразования ссылок (§10.2.8) при переадресовии типа System.Threading.Lock:

Неявные преобразования ссылок:

  • От любого reference_type до object и dynamic.
    • Предупреждение сообщается, когда reference_type, как известно, System.Threading.Lock.
  • От любого class_typeS до любого class_typeT, если S является производным от T.
    • выдаётся предупреждение, когда S известно как System.Threading.Lock.
  • От любых class_typeS к любым interface_typeT, при условии, что S реализует T.
    • выдается предупреждение, когда известно, что S является System.Threading.Lock.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Обратите внимание, что это предупреждение возникает даже для эквивалентных явных преобразований.

Компилятор не сообщает предупреждение в некоторых случаях, когда экземпляр не может быть заблокирован после преобразования в object:

  • Если преобразование неявно и является частью вызова оператора равенства объектов.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
    // ...

Чтобы выйти из предупреждения и принудительного использования блокировки на основе монитора, можно использовать

  • обычные средства подавления предупреждений (#pragma warning disable)
  • Monitor API напрямую,
  • неявное приведение, например object AsObject<T>(T l) => (object)l;.

Альтернативы

  • Поддержка общего шаблона, который другие типы также могут использовать для взаимодействия с ключевым словом lock. Это будущая работа, которая может быть реализована, когда ref structs может участвовать в универсальных шаблонах. Это обсуждалось в LDM 2023-12-04.

  • Чтобы избежать неоднозначности между мониторной блокировкой и новыми элементами типа Lock (или шаблоном в будущем), мы могли бы:

    • Введите новый синтаксис вместо повторного использования существующей инструкции lock.
    • Требовать, чтобы новые типы блокировки были struct(так как существующие lock запрещают типы значений). Могут возникнуть проблемы с конструкторами по умолчанию и копированием, если структуры имеют отложенную инициализацию.
  • Генератор кода может быть защищен от завершения потоков (которые сами устарели).

  • Мы также могли бы предупредить, когда Lock передается в качестве параметра типа, потому что блокировка на параметре типа всегда использует блокировку на основе монитора.

    M(new Lock()); // could warn here
    
    void M<T>(T x) // (specifying `where T : Lock` makes no difference)
    {
        lock (x) { } // because this uses Monitor
    }
    

    Однако это может вызвать предупреждения при хранении Lockв списке, что нежелательно.

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Можно включить статический анализ, чтобы предотвратить использование System.Threading.Lock в usingи await. То есть мы можем выдать ошибку или предупреждение для кода, например, using (lockVar.EnterScope()) { await ... }. В настоящее время это не требуется, так как Lock.Scope является ref struct, поэтому код является незаконным в любом случае. Однако, если бы мы когда-либо разрешили использовать ref structв методах async или изменили Lock.Scope, чтобы он не был ref struct, этот анализ стал бы полезным. (Мы также, вероятно, должны рассмотреть для этого все типы блокировки, соответствующие общему шаблону, в случае если они будут реализованы в будущем. Хотя может возникнуть необходимость в механизме отказа, поскольку некоторые типы блокировок могут быть разрешены для использования с await.) Кроме того, это может быть реализовано в виде анализатора, включенного в среду выполнения.

  • Мы могли бы ослабить ограничение, согласно которому типы значений не могут быть locked.

    • для нового типа Lock (только если предложение API изменило его с class на struct),
    • для общего шаблона, в котором при реализации в будущем сможет участвовать любой тип.
  • Мы могли бы разрешить новый lock в методах async, где await не используется внутри lock.

    • В настоящее время, так как lock снижается до using с ref struct в качестве ресурса, это приводит к ошибке во время компиляции. Решение заключается в извлечении lock в отдельный метод, не относящийся кasync.
    • Вместо использования ref struct Scopeмы могли бы выпустить методы Lock.Enter и Lock.Exit в try/finally. Однако метод Exit должен вызываться при вызове из другого потока, отличного от Enter, поэтому он содержит подстановку потока, которая избегается при использовании Scope.
    • Лучше всего разрешить компиляцию using на ref struct в методах async, если в теле using отсутствует await.

Совещания по проектированию

  • LDM 2023-05-01: первоначальное решение о поддержке шаблона lock
  • LDM 2023-10-16: включено в рабочий набор для .NET 9
  • LDM 2023-12-04: отклонил общий шаблон, приняв только специальные случаи для типа Lock и добавление предупреждений статического анализа