Freigeben über


Lock-Objekt

Anmerkung

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/7104

Zusammenfassung

Legen Sie einen Sonderfall fest, wie System.Threading.Lock mit dem Schlüsselwort lock interagiert (indem Sie dessen Methode EnterScope unter der Haube aufrufen). Fügen Sie statische Analysewarnungen hinzu, um versehentlichen Missbrauch des Typs nach Möglichkeit zu verhindern.

Motivation

.NET 9 führt einen neuen System.Threading.Lock Typ als bessere Alternative zur vorhandenen monitorbasierten Sperre ein. Wenn das Schlüsselwort lock in C# vorhanden ist, können Entwickler davon überzeugt sein, dass sie es mit diesem neuen Typ verwenden können. Dies würde nicht gemäß der Semantik dieses Typs sperren, sondern ihn wie jedes andere Objekt behandeln und monitorbasiertes Sperren verwenden.

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

Detailentwurf

Die Semantik der Lock-Anweisung (§13.13) wird geändert, um den System.Threading.Lock-Typ als Spezialfall zu behandeln:

Eine lock-Anweisung der Form lock (x) { ... }

  1. wobei x ein Ausdruck vom Typ System.Threading.Lockist, entspricht genau dem:
    using (x.EnterScope())
    {
        ...
    }
    
    und System.Threading.Lock müssen die folgende Form aufweisen:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. wobei x ein Ausdruck eines reference_type ist, ist genau äquivalent zu: [...]

Beachten Sie, dass das Shape möglicherweise nicht vollständig überprüft wird (z. B. gibt es keine Fehler oder Warnungen, wenn der Lock Typ nicht sealedist), aber das Feature funktioniert möglicherweise nicht wie erwartet (z. B. gibt es keine Warnungen beim Konvertieren von Lock in einen abgeleiteten Typ, da das Feature davon ausgeht, dass keine abgeleiteten Typen vorhanden sind).

Außerdem werden neue Warnungen zu impliziten Konversionen von Referenzen (§10.2.8) hinzugefügt, wenn der Typ System.Threading.Lock upcasted wird:

Die impliziten Verweiskonvertierungen sind:

  • Von jedem reference_type nach object und dynamic.
    • Eine Warnung wird geliefert, wenn der reference_type als System.Threading.Lock bekannt ist.
  • Von jedem class_typeS bis zu jeder class_typeT, vorausgesetzt, S wird von Tabgeleitet.
    • Eine Warnung wird gemeldet, wenn bekannt ist, dass SSystem.Threading.Lockist.
  • Von einem beliebigen class_typeS zu einem beliebigen interface_typeT, sofern S T implementiert.
    • Eine Warnung wird geliefert, wenn S als System.Threading.Lock bekannt ist.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Beachten Sie, dass diese Warnung auch bei entsprechenden expliziten Konvertierungen auftritt.

Der Compiler vermeidet die Meldung der Warnung in einigen Fällen, wenn die Instanz nach der Konvertierung in objectnicht gesperrt werden kann:

  • wenn die Konvertierung implizit und Teil eines Aufrufs eines Objektgleichheitsoperators ist.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
    // ...

Um die Warnung zu umgehen und die Verwendung von monitorbasiertem Sperren zu erzwingen, können Sie

  • die üblichen Mittel zur Unterdrückung von Warnungen verwenden (#pragma warning disable),
  • Monitor APIs direkt verwenden,
  • indirektes Casting wie object AsObject<T>(T l) => (object)l;.

Alternativen

  • Unterstützen Sie ein allgemeines Muster, das andere Typen auch für die Interaktion mit dem schlüsselwort lock verwenden können. Dies ist eine zukünftige Arbeit, die implementiert werden könnte, wenn ref structs an Generics teilnehmen können. Diskutiert in LDM 2023-12-04.

  • Um Mehrdeutigkeit zwischen der vorhandenen monitorbasierten Sperre und dem neuen Lock (oder zukünftigen Mustern) zu vermeiden, könnten wir:

    • Führen Sie eine neue Syntax ein, anstatt die vorhandene lock-Anweisung erneut zu verwenden.
    • Verlangen Sie, dass die neuen Lock-Typen structs sind (da das bestehende lock Wert-Typen nicht zulässt). Es können Probleme mit Standardkonstruktoren und Kopieren auftreten, wenn die Strukturen eine faule Initialisierung aufweisen.
  • Der Codegen könnte gegen Threadabbrüche gehärtet werden (die selbst veraltet sind).

  • Wir könnten auch warnen, wenn Lock als Typparameter übergeben wird, da beim Sperren eines Typparameters immer monitorbasierte Sperren verwendet werden:

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

    Dies würde jedoch Warnungen verursachen, wenn Locks in einer Liste gespeichert werden, die nicht erwünscht ist:

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Wir könnten eine statische Analyse einbauen, um die Verwendung von System.Threading.Lock in usings mit awaits zu verhindern. D.h., wir könnten entweder einen Fehler oder eine Warnung für Code wie using (lockVar.EnterScope()) { await ... }ausgeben. Derzeit ist dies nicht erforderlich, da Lock.Scope ein ref structist, damit der Code trotzdem illegal ist. Sollten wir jedoch jemals ref structs in async-Methoden zulassen oder Lock.Scope so ändern, dass es kein ref struct ist, würde diese Analyse von Nutzen sein. (Wir müssten wahrscheinlich auch alle Lock-Typen berücksichtigen, die dem allgemeinen Muster entsprechen, falls dies in Zukunft implementiert wird. Allerdings müsste es einen Opt-Out-Mechanismus geben, da einige Lock-Typen die Möglichkeit bieten könnten, mit await verwendet zu werden.) Alternativ könnte dies als ein Analysator implementiert werden, der als Element der Runtime ausgeliefert wird.

  • Wir könnten die Einschränkung lockern, dass Wertetypen nicht locked werden können.

    • für den neuen Lock-Typ (nur erforderlich, wenn der API-Vorschlag den Typ von class in structgeändert hat),
    • für das allgemeine Muster, an dem jeder Typ teilnehmen kann, wenn er in Zukunft implementiert wird.
  • Wir konnten die neue lock in async Methoden zulassen, bei denen await nicht innerhalb der lockverwendet wird.

    • Aktuell führt dies zu einem Kompilierfehler, da lock zu using mit einer ref struct als Ressource herabgesetzt wird. Die Abhilfe besteht darin, das lock in eine separate Nicht-async-Methode zu extrahieren.
    • Anstatt die ref struct Scopezu verwenden, könnten wir Lock.Enter und Lock.Exit Methoden in try/finallyausgeben. Die Exit-Methode muss jedoch ausgelöst werden, wenn sie von einem anderen Thread als Enteraufgerufen wird, daher enthält sie eine Threadsuche, die bei Verwendung der Scopevermieden wird.
    • Am besten wäre es, die Kompilierung von using auf einem ref struct in async Methoden zuzulassen, wenn es kein await innerhalb des using Bodys gibt.

Design-Besprechungen

  • LDM 2023-05-01: Erste Entscheidung, ein lock-Muster zu unterstützen
  • LDM 2023-10-16: In das Arbeits-Set für .NET 9 festgelegt
  • LDM 2023-12-04: verwarf das allgemeine Muster, akzeptierte nur die Sonderkodierung des Lock Typs + Hinzufügen von Warnungen zur statischen Analyse