Compartilhar via


objeto Lock

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).

Você pode saber mais sobre o processo de adoção de especificações de funcionalidades no padrão da linguagem C# no artigo sobre as especificações .

Problema do especialista: https://github.com/dotnet/csharplang/issues/7104

Resumo

Caso especial de como System.Threading.Lock interage com a palavra-chave lock (chamando seu método EnterScope nos bastidores). Adicione avisos de análise estática para evitar o uso indevido acidental do tipo sempre que possível.

Motivação

O .NET 9 está introduzindo um novo tipo de System.Threading.Lock como uma alternativa melhor ao bloqueio baseado em monitor existente. A presença da palavra-chave lock em C# pode levar os desenvolvedores a pensar que podem usá-la com esse novo tipo. Fazer isso não causaria bloqueio de acordo com a semântica desse tipo, mas o trataria como qualquer outro objeto e usaria o bloqueio baseado em monitor.

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

Design detalhado

A semântica da instrução de bloqueio (§13.13) é alterada para tratar o tipo System.Threading.Lock de forma especial.

Uma declaração lock do formulário lock (x) { ... }

  1. em que x é uma expressão do tipo System.Threading.Lock, é exatamente equivalente a:
    using (x.EnterScope())
    {
        ...
    }
    
    e System.Threading.Lock devem ter a seguinte forma:
    namespace System.Threading
    {
        public sealed class Lock
        {
            public Scope EnterScope();
    
            public ref struct Scope
            {
                public void Dispose();
            }
        }
    }
    
  2. em que x é uma expressão de um reference_type, é exatamente equivalente a: [...]

Observe que a forma pode não ser totalmente verificada (por exemplo, não haverá erros nem avisos se o tipo de Lock não for sealed), mas o recurso pode não funcionar conforme o esperado (por exemplo, não haverá avisos ao converter Lock em um tipo derivado, pois o recurso pressupõe que não há tipos derivados).

Além disso, novos avisos são adicionados a conversões de referência implícitas (§10.2.8) ao fazer o upcast do tipo System.Threading.Lock:

As conversões de referência implícitas são:

  • De qualquer reference_type a object e dynamic.
    • Um aviso é emitido quando o reference_type é conhecido como System.Threading.Lock.
  • De qualquer class_typeS a qualquer class_typeT, desde que S seja derivado de T.
    • Um aviso é emitido quando S é conhecido por ser System.Threading.Lock.
  • De qualquer class_typeS para qualquer interface_typeT, desde que S implemente T.
    • Um aviso é relatado quando S é conhecido como System.Threading.Lock.
  • [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here

Observe que esse aviso ocorre mesmo para conversões explícitas equivalentes.

O compilador evita relatar o aviso em alguns casos em que a instância não pode ser bloqueada após a conversão em object:

  • quando a conversão estiver implícita e parte de uma invocação de operador de igualdade 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 escapar do aviso e forçar o uso do bloqueio baseado em monitor, é possível usar

  • os meios usuais de supressão de aviso (#pragma warning disable),
  • Monitor APIs diretamente,
  • conversão indireta como object AsObject<T>(T l) => (object)l;.

Alternativas

  • Dê suporte a um padrão geral que outros tipos também podem usar para interagir com a palavra-chave lock. Esse é um trabalho futuro que pode ser implementado quando ref structs puderem participar de genéricos. Discutido em LDM 2023-12-04.

  • Para evitar ambiguidade entre o bloqueio baseado em monitor existente e o novo Lock (ou padrão no futuro), poderíamos:

    • Introduza uma nova sintaxe em vez de reutilize a instrução lock existente.
    • Exija que os novos tipos de bloqueio sejam structs (já que o lock existente não permite tipos de valor). Pode haver problemas com construtores padrão e cópia se os structs tiverem inicialização lenta.
  • O codegen pode ser protegido contra anulações de thread (em que são eles mesmos obsoletos).

  • Podemos avisar também quando Lock é passado como um parâmetro de tipo, pois o bloqueio em um parâmetro de tipo sempre usa o bloqueio baseado em 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
    }
    

    No entanto, isso causaria avisos ao armazenar Locks em uma lista indesejável:

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Podemos incluir análise estática para impedir o uso de System.Threading.Lock em usings com awaits. Ou seja, poderíamos emitir um erro ou um aviso para um código como using (lockVar.EnterScope()) { await ... }. Atualmente, isso não é necessário, pois Lock.Scope é um ref struct, de modo que o código seja ilegal de qualquer maneira. No entanto, se alguma vez permitirmos ref structem métodos async ou mudássemos Lock.Scope para deixar de ser um ref struct, essa análise se tornaria benéfica. (Provavelmente, também precisaríamos considerar todos os tipos de bloqueio que correspondem ao padrão geral se implementados no futuro. Embora talvez seja necessário haver um mecanismo de exclusão, pois alguns tipos de bloqueio podem ter permissão para serem usados com await.) Alternativamente, isso pode ser implementado como um analisador enviado como parte do tempo de execução.

  • Poderíamos relaxar a restrição de que os tipos de valor não podem ser locked

    • para o novo tipo de Lock (necessário somente se a proposta da API a alterou de class para struct),
    • para o modelo geral em que qualquer tipo poderá participar quando for implementado no futuro.
  • Poderíamos permitir o novo lock em métodos async em que await não é usado dentro do lock.

    • Atualmente, já que lock é transformado em using utilizando ref struct como recurso, isso resulta em um erro de compilação. A solução alternativa é extrair o lock em um método nãoasync separado.
    • Em vez de usar o ref struct Scope, poderíamos emitir métodos Lock.Enter e Lock.Exit em try/finally. No entanto, o método Exit deve ser lançado quando é chamado a partir de um thread diferente de Enter, portanto, ele contém uma pesquisa de thread que é evitada ao usar o Scope.
    • Seria melhor permitir a compilação de using em um ref struct nos métodos async, caso não haja await dentro do corpo de using.

Reuniões de design

  • LDM 2023-05-01: decisão inicial de dar suporte ao padrão lock
  • LDM 2023-10-16: classificado no conjunto de trabalho do .NET 9
  • LDM 2023-12-04: rejeitou o padrão geral, aceitou apenas a caixa especial do tipo Lock + adicionando avisos de análise estática