Condividi tramite


oggetto Lock

Nota

Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono registrate nelle pertinenti note del language design meeting (LDM).

Altre informazioni sul processo di adozione delle speclet di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche .

Problema del campione: https://github.com/dotnet/csharplang/issues/7104

Sommario

Caso speciale su come System.Threading.Lock interagisce con la parola chiave lock (chiamando in modo nascosto il relativo metodo EnterScope). Aggiungere avvisi di analisi statica per evitare un uso improprio accidentale del tipo, se possibile.

Motivazione

.NET 9 introduce un nuovo tipo di System.Threading.Lock come alternativa migliore al blocco basato sul monitoraggio esistente. La presenza della parola chiave lock in C# potrebbe portare gli sviluppatori a pensare che possano usarla con questo nuovo tipo. In tal modo, non si bloccherebbe secondo le semantiche di questo tipo, ma verrebbe trattato come qualsiasi altro oggetto e si utilizzerebbe un blocco basato sul monitor.

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

Progettazione dettagliata

La semantica dell'istruzione lock (§13.13) è stata modificata per trattare il tipo System.Threading.Lock come un caso speciale.

Istruzione lock del modulo lock (x) { ... }

  1. dove x è un'espressione di tipo System.Threading.Lock, equivale esattamente a:
    using (x.EnterScope())
    {
        ...
    }
    
    e System.Threading.Lock devono avere la forma seguente:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. dove x è un'espressione di un reference_type, è esattamente equivalente a: [...]

Si noti che la forma potrebbe non essere completamente controllata (ad esempio, non ci saranno errori né avvisi se il tipo di Lock non è sealed), ma la funzionalità potrebbe non funzionare come previsto (ad esempio, non ci saranno avvisi durante la conversione di Lock in un tipo derivato, poiché la funzionalità presuppone che non ci siano tipi derivati).

Inoltre, vengono aggiunti nuovi avvisi alle conversioni di riferimento implicite (§10.2.8) durante l'upcast del tipo di System.Threading.Lock:

Le conversioni di riferimento implicite sono:

  • Da qualsiasi reference_type a object e dynamic.
    • Viene segnalato un avviso quando il reference_type è noto come System.Threading.Lock.
  • Da qualsiasi class_typeS a qualsiasi class_typeT, a condizione che S derivi da T.
    • Viene segnalato un avviso quando S è noto come System.Threading.Lock.
  • Da qualsiasi class_typeS a qualsiasi interface_typeT, a condizione che S implementi T.
    • Viene segnalato un avviso quando S è noto come System.Threading.Lock.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Si noti che questo avviso si verifica anche per conversioni esplicite equivalenti.

Il compilatore evita di segnalare l'avviso in alcuni casi quando l'istanza non può essere bloccata dopo la conversione in object:

  • quando la conversione è implicita e parte della chiamata di un operatore di uguaglianza di oggetti.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
    // ...

Per uscire dall'avviso e forzare l'uso del blocco basato sul monitoraggio, è possibile usare

  • i soliti mezzi di soppressione degli avvisi (#pragma warning disable)
  • Monitor API direttamente,
  • cast indiretto come object AsObject<T>(T l) => (object)l;.

Alternative

  • Supportare un modello generale che altri tipi possono usare anche per interagire con la parola chiave lock. Si tratta di un lavoro futuro che potrebbe essere implementato quando ref structpuò partecipare ai generics. Descritto in LDM 2023-12-04.

  • Per evitare ambiguità tra il blocco basato sul monitoraggio esistente e il nuovo Lock (o modello in futuro), è possibile:

    • Introdurre una nuova sintassi anziché riutilizzare l'istruzione lock esistente.
    • Richiedere che i nuovi tipi di blocco siano struct(poiché il lock esistente non consente tipi valore). Potrebbero verificarsi problemi con i costruttori predefiniti e la copia se gli struct hanno inizializzazione differita.
  • Il codegen può essere sottoposto a protezione avanzata contro interruzioni del thread (che sono essi stessi obsoleti).

  • È possibile avvisare anche quando Lock viene passato come parametro di tipo, perché bloccare un parametro di tipo utilizza sempre il blocco basato sul monitor.

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

    Tuttavia, ciò causerebbe avvisi durante l'archiviazione di Lockin un elenco che è indesiderato:

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • È possibile includere l'analisi statica per impedire l'uso di System.Threading.Lock in usingcon await. Ad esempio, è possibile generare un errore o un avviso per il codice come using (lockVar.EnterScope()) { await ... }. Attualmente, questo non è necessario perché Lock.Scope è un ref struct, in modo che il codice sia comunque illegale. Tuttavia, se permettessimo mai ref structnei metodi async o modificassimo Lock.Scope per non essere un ref struct, questa analisi diventerebbe utile. È probabile che sia necessario prendere in considerazione anche per questo tutti i tipi di blocco corrispondenti al modello generale, se implementato in futuro. Anche se potrebbe essere necessario un meccanismo di rifiuto esplicito perché alcuni tipi di blocco potrebbero essere autorizzati a essere usati con await. In alternativa, questo può essere implementato come analizzatore fornito come parte del runtime.

  • È possibile ridurre la restrizione che i tipi di valore non possono essere locked

    • per il nuovo tipo di Lock (necessario solo se la proposta dell'API lo ha modificato da class a struct),
    • per il modello generale in cui qualsiasi tipo potrà partecipare una volta implementato in futuro.
  • È possibile consentire la nuova lock nei metodi async in cui await non viene usato all'interno del lock.

    • Attualmente, poiché lock viene ridotto a using con un ref struct come risorsa, viene generato un errore in fase di compilazione. La soluzione alternativa consiste nell'estrarre il lock in un metodo nonasync separato.
    • Anziché usare il ref struct Scope, è possibile generare metodi Lock.Enter e Lock.Exit in try/finally. Tuttavia, il metodo Exit deve generare quando viene chiamato da un thread diverso da Enter, pertanto contiene una ricerca di thread che viene evitata quando si usa il Scope.
    • Sarebbe meglio consentire la compilazione di using in un ref struct nei metodi async se non ci sono await all'interno del corpo using.

Riunioni di progettazione

  • LDM 2023-05-01: decisione iniziale di supportare un modello di lock
  • LDM 2023-10-16: incluso nel set di lavoro per .NET 9
  • LDM 2023-12-04: rifiutato il modello generale, accettata solo la gestione dei casi speciali per il tipo Lock + in aggiunta avvisi di analisi statica