Lock objekt

Anteckning

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningarna (Language Design Meeting) .

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-utgåva: https://github.com/dotnet/csharplang/issues/7104

Sammanfattning

Specialfall hur System.Threading.Lock interagerar med nyckelordet lock (anropar dess EnterScope metod under huven). Lägg till statiska analysvarningar för att förhindra oavsiktligt missbruk av typen där det är möjligt.

Motivation

.NET 9 introducerar en ny System.Threading.Lock typ som ett bättre alternativ till befintlig övervakningsbaserad låsning. Förekomsten av nyckelordet lock i C# kan leda till att utvecklare tror att de kan använda det med den här nya typen. Detta skulle inte låsas enligt denna typs semantik, utan skulle i stället behandlas som alla andra objekt och använda monitorbaserad låsning.

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

Detaljerad design

Semantik för låssatsen (§13.13) ändras för att specialhantera System.Threading.Lock-typen.

En lock-deklaration av formen lock (x) { ... }

  1. där x är ett uttryck av typen System.Threading.Lock, är exakt detsamma som:
    using (x.EnterScope())
    {
        ...
    }
    
    och System.Threading.Lock måste ha följande form:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. där x är ett uttryck för en reference_type, är exakt detsamma som: [...]

Observera att formen kanske inte är helt markerad (t.ex. kommer det inte att finnas några fel eller varningar om den Lock typen inte är sealed), men funktionen kanske inte fungerar som förväntat (t.ex. kommer det inte att finnas några varningar när du konverterar Lock till en härledd typ, eftersom funktionen förutsätter att det inte finns några härledda typer).

Dessutom läggs nya varningar till implicita referenskonverteringar (§10.2.8) vid uppkonvertering av typen System.Threading.Lock.

De implicita referenskonverteringarna är:

  • Från alla referenstyp till object och dynamic.
    • En varning rapporteras när reference_type är känd som System.Threading.Lock.
  • Från alla class_typeS till alla class_typeT, förutsatt att S härleds från T.
    • En varning rapporteras när S är känd som System.Threading.Lock.
  • Från alla class_typeS till alla interface_typeT, förutsatt att S implementerar T.
    • En varning utfärdas när S är känd som System.Threading.Lock.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Observera att den här varningen inträffar även för motsvarande explicita konverteringar.

Kompilatorn undviker att rapportera varningen i vissa fall när instansen inte kan låsas efter konvertering till object:

  • när konverteringen är implicit och en del av ett anrop för objektjämlikhetsoperator.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
    // ...

För att komma undan varningen och tvinga fram användning av övervakningsbaserad låsning kan man använda

  • det vanliga varningsdämpningsmedlet (#pragma warning disable),
  • Monitor API direkt,
  • indirekt gjutning som object AsObject<T>(T l) => (object)l;.

Alternativ

  • Stöd för ett allmänt mönster som andra typer också kan använda för att interagera med nyckelordet lock. Det här är ett framtida arbete som kan implementeras när ref structkan delta i generiska program. Diskuterad i LDM 2023-12-04.

  • För att undvika tvetydigheter mellan den befintliga övervakningsbaserade låsningen och den nya Lock (eller mönster i framtiden) kan vi:

    • Introducera en ny syntax i stället för att återanvända den befintliga lock-instruktionen.
    • Kräv att de nya låstyperna ska vara struct(eftersom den befintliga lock tillåter inte värdetyper). Det kan uppstå problem med standardkonstruktorer och kopiering om strukturerna har fördröjd initiering.
  • Kodgenen kan härdas mot tråd aborter (som i sig är föråldrade).

  • Vi kan också varna när Lock skickas som en typparameter, eftersom låsning på en typparameter alltid använder övervakningsbaserad låsning:

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

    Det skulle dock orsaka varningar när Locklagras i en lista som är oönskad:

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Vi kan inkludera statisk analys för att förhindra användning av System.Threading.Lock i usings med awaits. Vi kan antingen generera ett fel eller en varning för kod som using (lockVar.EnterScope()) { await ... }. För närvarande behövs detta inte eftersom Lock.Scope är en ref struct, så att koden ändå är ogiltig. Men om vi någonsin tillät ref structs i async-metoder eller ändrade Lock.Scope så att det inte längre är en ref struct, skulle denna analys bli till nytta. (Vi skulle sannolikt också behöva överväga detta för alla låstyper som matchar det allmänna mönstret, om det implementeras vid behov i framtiden. Även om det kan behövas en möjlighet att välja bort, eftersom vissa låstyper kan tillåtas användas med await.) Alternativt kan detta implementeras som en analysator som levereras som en del av körexekveringen.

  • Vi kan lätta på begränsningen som gör att värdetyper inte kan locked.

    • för den nya Lock typen (behövs bara om API-förslaget ändrade det från class till struct),
    • för det allmänna mönstret där alla typer kan delta när de implementeras i framtiden.
  • Vi kan tillåta den nya lock i async metoder där await inte används i lock.

    • Eftersom lock för närvarande sänks till using med en ref struct som resurs resulterar detta i ett kompileringsfel. Lösningen är att extrahera lock till en separat icke-async metod.
    • I stället för att använda ref struct Scopekan vi generera Lock.Enter och Lock.Exit metoder i try/finally. Men metoden Exit måste utlösas när den anropas från en annan tråd än Enter, och därför innehåller den en trådsökning som undviks när du använder Scope.
    • Det bästa vore att tillåta kompilering av using på en ref struct inom async-metoder om det inte finns några await i using koden.

Designa möten

  • LDM 2023-05-01: första beslutet att stödja ett lock mönster
  • LDM 2023-10-16: sorteras i arbetsuppsättningen för .NET 9
  • LDM 2023-12-04: avvisade det allmänna mönstret, accepterade endast specialhölje Lock typ + lägga till statiska analysvarningar