Partilhar via


Lock objeto

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações 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 Language Design Meeting (LDM).

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

Questão campeã: https://github.com/dotnet/csharplang/issues/7104

Resumo

Especificar como System.Threading.Lock interage com a palavra-chave lock (chamando o seu método EnterScope em segundo plano). 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 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 bloquearia segundo a semântica desse tipo, mas sim tratá-lo-ia 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();
        }
    }
}

Projeto detalhado

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

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

  1. em que x é uma expressão do tipo System.Threading.Lock, é precisamente 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. onde x é uma expressão de um reference_type, é precisamente equivalente a: [...]

Observe que a forma pode não estar totalmente verificada (por exemplo, não haverá erros nem avisos se o tipo Lock não for sealed), mas o recurso pode não funcionar como esperado (por exemplo, não haverá avisos ao converter Lock em um tipo derivado, já que o recurso assume que não há tipos derivados).

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

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

  • De qualquer tipo de referência a object e dynamic.
    • Um aviso é gerado quando é sabido que o reference_type está System.Threading.Lock.
  • De qualquer class_typeS a qualquer class_typeT, desde que S derive de T.
    • Um aviso é emitido quando se sabe que SSystem.Threading.Lock.
  • De qualquer class_typeS para qualquer interface_typeT, desde que S implemente T.
    • Um aviso é comunicado quando se sabe que S está 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 quando a instância não pode ser bloqueada após a conversão para object:

  • quando a conversão é implícita e parte de uma invocação do operador de igualdade de objeto.
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, pode-se usar

  • os meios habituais de supressão dos avisos (#pragma warning disable),
  • Monitor APIs diretamente,
  • casting indireto como object AsObject<T>(T l) => (object)l;.

Alternativas

  • Suporte a um padrão geral que outros tipos também podem usar para interagir com a palavra-chave lock. Este é um trabalho futuro que poderá ser implementado quando ref structpuder participar em 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 reutilizar a instrução lock existente.
    • Exija que os novos tipos de bloqueio sejam structs (uma vez que o lock existente não permite tipos de valor). Pode haver problemas com construtores padrão e operações de cópia se as structs tiverem inicialização lenta.
  • O codegen pode ser fortalecido contra interrupções de threads (que estão obsoletos).

  • Poderíamos avisar também quando Lock é passado como um parâmetro de tipo, porque o bloqueamento em um parâmetro de tipo sempre usa bloqueamento 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 que é indesejável:

    List<Lock> list = new();
    list.Add(new Lock()); // would warn here
    
  • Poderíamos incluir a análise estática para evitar o uso de System.Threading.Lock em usings com awaits. Ou seja, podemos emitir um erro ou um aviso para um código como using (lockVar.EnterScope()) { await ... }. Atualmente, isso não é necessário, uma vez que Lock.Scope é um ref struct, de modo que o código é ilegal de qualquer maneira. No entanto, se alguma vez permitíssemos ref structs em métodos async ou mudássemos Lock.Scope para não ser um ref struct, esta análise tornar-se-ia benéfica. (Também provavelmente precisaríamos considerar para isso todos os tipos de bloqueio correspondentes ao padrão geral, se implementados no futuro. Embora possa ser necessário um mecanismo de exclusão, pois alguns tipos de bloqueio podem ser permitidos para serem usados com await.) Como alternativa, isso pode ser implementado como um analisador enviado como parte do tempo de execução.

  • Poderíamos flexibilizar a restrição de que os tipos de valores não podem ser lock

    • para o novo tipo de Lock (necessário apenas se a proposta da API o alterou de class para struct),
    • para o padrão geral no qual qualquer tipo pode participar quando implementado no futuro.
  • Poderíamos permitir a nova lock nos async métodos onde await não é utilizado dentro do lock.

    • Atualmente, como lock é reduzido para using com um ref struct como recurso, isso resulta em um erro em tempo 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 os métodos Lock.Enter e Lock.Exit em try/finally. No entanto, o método Exit deve ser lançado quando é chamado de um thread diferente de Enter, portanto, ele contém uma pesquisa de thread que é evitada ao usar o Scope.
    • O melhor seria permitir a compilação de using em um ref struct em métodos async se não houver await dentro do corpo using.

Reuniões de design

  • LDM 2023-05-01: decisão inicial de apoiar um padrão lock
  • LDM 2023-10-16: incluído no conjunto de trabalho para .NET 9
  • LDM 2023-12-04: rejeitou o padrão geral, aceitou apenas a caixa especial do tipo Lock + adição de avisos de análise estática