Udostępnij za pośrednictwem


obiekt Lock

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są odnotowane w odpowiednich notatkach ze spotkania dotyczącego projektowania języka (LDM) .

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Problem z mistrzem: https://github.com/dotnet/csharplang/issues/7104

Streszczenie

Szczególny przypadek interakcji System.Threading.Lock ze słowem kluczowym lock (wywoływanie metody EnterScope w tle). Dodaj ostrzeżenia analizy statycznej, aby zapobiec przypadkowemu niewłaściwemu użyciu typu, jeśli jest to możliwe.

Motywacja

Platforma .NET 9 wprowadza nowy typ System.Threading.Lock jako lepszą alternatywę dla istniejących blokad opartych na monitorach. Obecność słowa kluczowego lock w języku C# może prowadzić deweloperów do myślenia, że mogą używać go z tym nowym typem. W ten sposób nie zablokowałby się zgodnie z semantyką tego typu, ale zamiast tego traktowałby go jak każdy inny obiekt i używałby blokady opartej na monitorze.

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

Szczegółowy projekt

Semantyka instrukcji blokady (§13.13) zmienia się, aby traktować typ System.Threading.Lock jako przypadek szczególny.

Oświadczenie lock w formie lock (x) { ... }

  1. gdzie x jest wyrażeniem typu System.Threading.Lock, jest dokładnie równoważne:
    using (x.EnterScope())
    {
        ...
    }
    
    i System.Threading.Lock muszą mieć następujący kształt:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. gdzie x jest wyrażeniem reference_type, jest dokładnie równoważne: [...]

Należy pamiętać, że kształt może nie zostać w pełni sprawdzony (np. nie będzie żadnych błędów ani ostrzeżeń, jeśli typ Lock nie jest sealed), ale funkcja może nie działać zgodnie z oczekiwaniami (np. podczas konwertowania Lock na typ pochodny, ponieważ funkcja zakłada, że nie ma żadnych typów pochodnych).

Ponadto nowe ostrzeżenia są dodawane do niejawnych konwersji odwołań (§10.2.8) podczas podnoszenia typu System.Threading.Lock.

Konwersje odwołań niejawnych to:

  • Z dowolnego reference_type do object i dynamic.
    • Ostrzeżenie jest zgłaszane, gdy reference_type jest znany jako System.Threading.Lock.
  • Od dowolnego class_typeS do dowolnego class_typeT, pod warunkiem, że S pochodzi z T.
    • ostrzeżenie jest zgłaszane, gdy S jest znany jako System.Threading.Lock.
  • Z dowolnego class_typeS do dowolnego interface_typeT, pod warunkiem, że S implementuje T.
    • ostrzeżenie jest zgłaszane, gdy S jest znany jako System.Threading.Lock.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Należy pamiętać, że to ostrzeżenie pojawi się nawet przy równoważnych jawnych konwersjach.

Kompilator unika raportowania ostrzeżenia w niektórych przypadkach, gdy nie można zablokować wystąpienia po przekonwertowaniu na object:

  • gdy konwersja jest niejawna i stanowi część wywołania operatora porównania obiektu.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
    // ...

Aby uciec przed ostrzeżeniem i wymusić użycie blokady opartej na monitorze, można użyć

  • zwykłe środki tłumienia ostrzeżeń (#pragma warning disable),
  • API Monitor bezpośrednio,
  • rzutowanie pośrednie, takie jak np. object AsObject<T>(T l) => (object)l;.

Alternatywy

  • Obsługa ogólnego wzorca, którego mogą również używać inne typy do interakcji ze słowem kluczowym lock. Jest to przyszły projekt, który może zostać wdrożony, gdy ref structbędzie mógł brać udział w uogólnieniach. Omówiono w LDM 2023-12-04.

  • Aby uniknąć niejednoznaczności między istniejącym blokowaniem opartym na monitorze a nowym Lock (lub wzorcem w przyszłości), możemy:

    • Wprowadzenie nowej składni zamiast ponownego użycia istniejącej instrukcji lock.
    • Należy wymagać, aby nowe typy blokad były struct, ponieważ istniejące lock nie zezwalają na typy wartości. Mogą wystąpić problemy z konstruktorami domyślnymi i kopiowaniem, jeśli struktury mają opóźnioną inicjację.
  • Generowanie kodu może być wzmocnione przeciwko przerwaniom wątków (które same w sobie są przestarzałe).

  • Możemy również ostrzegać, gdy Lock jest przekazywany jako parametr typu, ponieważ blokowanie parametru typu zawsze używa blokady opartej na monitorze:

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

    Spowoduje to jednak ostrzeżenia podczas przechowywania Locks na liście, co jest niepożądane.

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Możemy uwzględnić analizę statyczną, aby zapobiec użyciu System.Threading.Lock w usings z awaits. Tj. możemy wyemitować błąd lub ostrzeżenie dotyczące kodu, takiego jak using (lockVar.EnterScope()) { await ... }. Obecnie nie jest to potrzebne, ponieważ Lock.Scope jest ref struct, więc kod jest mimo to nielegalny. Jeśli jednak kiedykolwiek pozwolilibyśmy ref structw metodach async lub zmienilibyśmy Lock.Scope, aby nie było ref struct, ta analiza stałaby się użyteczna. (Proponujemy również rozważyć wszystkie typy blokad zgodne z ogólnym wzorcem, jeśli zostaną wdrożone w przyszłości. Może być jednak konieczny mechanizm wyłączenia, ponieważ niektóre typy blokad mogą być używane z await. Alternatywnie, można to wdrożyć jako analizator zapewniany w ramach środowiska uruchomieniowego.)

  • Możemy złagodzić ograniczenie, że typy wartości nie mogą być locked

    • dla nowego typu Lock (wymagane tylko wtedy, gdy propozycja interfejsu API zmieniła go z class na struct),
    • dla ogólnego wzorca, w którym dowolny typ może uczestniczyć w przypadku implementacji w przyszłości.
  • Możemy zezwolić na nowe lock w metodach async, jeśli await nie jest używana wewnątrz lock.

    • Obecnie, ponieważ lock jest obniżony do using przy użyciu ref struct jako zasobu, powoduje to błąd czasu kompilacji. Obejściem jest wyodrębnienie lock do oddzielnej metody innej niżasync.
    • Zamiast używać ref struct Scope, możemy emitować metody Lock.Enter i Lock.Exit w try/finally. Jednak metoda Exit musi zostać wyrzucona, gdy jest wywoływana z innego wątku niż Enter, dlatego zawiera wyszukiwanie wątków, którego unika się podczas korzystania z Scope.
    • Najlepiej byłoby zezwolić na kompilację using na ref struct w metodach async, jeśli nie ma await wewnątrz ciała using.

Spotkania projektowe

  • LDM 2023-05-01: początkowa decyzja o wsparciu wzorca lock
  • LDM 2023-10-16: przeprowadzono triage do zestawu roboczego dla platformy .NET 9
  • LDM 2023-12-04: odrzucił ogólny wzorzec, zaakceptował tylko specjalne traktowanie typu Lock i dodanie ostrzeżeń analizy statycznej