Compartir vía


Objeto Lock

Nota

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .

Problema planteado por experto: https://github.com/dotnet/csharplang/issues/7104

Resumen

Caso especial de cómo interactúa System.Threading.Lock con la palabra clave lock (llamando al método EnterScope en segundo plano). Agregue advertencias de análisis estáticos para evitar un uso incorrecto accidental del tipo siempre que sea posible.

Motivación

.NET 9 presenta un nuevo tipo de System.Threading.Lock como mejor alternativa al bloqueo basado en monitor existente. La presencia de la palabra clave lock en C# podría llevar a los desarrolladores a pensar que pueden usarla con este nuevo tipo. Si lo hace, no se bloquearía según la semántica de este tipo, sino que lo trataría como cualquier otro objeto y usaría el bloqueo basado en el monitor.

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

Diseño detallado

La semántica de la instrucción de lock (§13.13) se modifica para tratar de manera especial el tipo System.Threading.Lock:

Instrucción lock de la forma lock (x) { ... }

  1. donde x es una expresión de tipo System.Threading.Lock, es exactamente equivalente a:
    using (x.EnterScope())
    {
        ...
    }
    
    y System.Threading.Lock deben tener la siguiente forma:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. donde x es una expresión de un reference_type, es exactamente equivalente a: [...]

Tenga en cuenta que es posible que la forma no esté totalmente activada (por ejemplo, no habrá errores ni advertencias si el tipo Lock no es sealed), pero es posible que la característica no funcione según lo previsto (por ejemplo, no habrá advertencias al convertir Lock a un tipo derivado, ya que la característica supone que no hay ningún tipo derivado).

Además, se han añadido nuevas advertencias a conversiones de referencia implícitas (§10.2.8) al convertir el tipo System.Threading.Lock:

Las conversiones de referencia implícitas son:

  • De reference_type a object y dynamic.
    • Se genera una advertencia cuando se sabe que el reference_type es System.Threading.Lock.
  • De cualquier class_typeS a cualquier class_typeT, siempre que S se derive de T.
    • Se notifica una advertencia cuando se sabe que S es System.Threading.Lock.
  • Desde cualquier class_typeS a cualquier interface_typeT, siempre que S implemente T.
    • Se notifica una advertencia cuando se sabe que S es System.Threading.Lock.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Tenga en cuenta que esta advertencia se produce incluso para conversiones explícitas equivalentes.

El compilador evita notificar la advertencia en algunos casos cuando la instancia no se puede bloquear después de convertir en object:

  • cuando la conversión es implícita y forma parte de una invocación de operador de igualdad de objetos.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
    // ...

Para saltarse la advertencia y forzar el uso del bloqueo basado en el monitor, se puede emplear lo siguiente

  • los medios habituales de eliminación de advertencias (#pragma warning disable),
  • las API de Monitor directamente, o
  • la conversión indirecta como object AsObject<T>(T l) => (object)l;.

Alternativas

  • Admita un patrón general que otros tipos también pueden usar para interactuar con la palabra clave lock. Esto es algo que se tiene previsto implementar cuando las variables ref structs puedan intervenir en los genéricos. Hay un hilo explicativo sobre esto en LDM 2023-12-04.

  • Para evitar ambigüedad entre el bloqueo basado en el monitor existente y el nuevo Lock (o patrón en el futuro), podríamos:

    • Introduce una nueva sintaxis en lugar de reutilizar la instrucción lock existente.
    • Requerir que los nuevos tipos de bloqueo sean structs (ya que el lock existente no permite los tipos de valor). Podría haber problemas con los constructores predeterminados y las copias si las estructuras tienen una inicialización diferida.
  • La generación de código se podría proteger frente a anulaciones de subprocesos (que están obsoletas).

  • También podríamos avisar cuando se pasa Lock como parámetro de tipo, ya que el bloqueo en un parámetro de tipo siempre utiliza un bloqueo basado en el 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
    }
    

    Sin embargo, esto provocaría advertencias al almacenar Locken una lista que no es deseable:

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Podríamos incluir análisis estáticos para evitar el uso de System.Threading.Lock en variables usings con awaits. Por ejemplo, podríamos emitir un error o una advertencia para código como using (lockVar.EnterScope()) { await ... }. Actualmente, esto no es necesario, ya que Lock.Scope es un ref struct, por lo que el código es ilegal de todos modos. Sin embargo, si alguna vez permitimos ref structen métodos async o cambiamos Lock.Scope para que deje de ser un ref struct, este análisis podría ser beneficioso. (Es probable que también tengamos que considerar todos los tipos de bloqueo que coincidan con el patrón general si se implementa en el futuro. Aunque es posible que deba haber un mecanismo de exclusión, ya que es posible que algunos tipos de bloqueo se puedan usar con await). Como alternativa, esto podría implementarse como analizador enviado como parte del entorno de ejecución.

  • Podríamos relajar la restricción de que los tipos de valor no se puedan bloquear (lock)

    • para el nuevo tipo Lock (es necesario solo si la propuesta de la API lo cambió de class a struct),
    • en el patrón general en el que cualquier tipo puede intervenir cuando se implemente en el futuro.
  • Podríamos permitir el nuevo lock en métodos async en los que await no se usa dentro del lock.

    • Actualmente, dado que lock se reduce a using con un ref struct como recurso, se produce un error en tiempo de compilación. La solución consiste en extraer el lock en un método noasync independiente.
    • En lugar de usar el ref struct Scope, podríamos emitir métodos Lock.Enter y Lock.Exit en try/finally. Sin embargo, el método Exit debe iniciarse cuando se llama a partir de un subproceso diferente de Enter, ya que incluye una búsqueda de subprocesos que se evita al usar el Scope.
    • Lo mejor sería que se permitiera la compilación de using en un ref struct en métodos async si no hay await dentro del cuerpo de using.

Reuniones de diseño

  • LDM 2023-05-01: Decisión inicial de incorporar un patrón lock
  • LDM 2023-10-16: Probado en el espacio de trabajo de .NET 9
  • LDM 2023-12-04: Se ha rechazado el patrón general, solo se ha aceptado los casos especiales del tipo Lock y se han añadido advertencias de análisis estático