Tenho uma variável de instância ou propriedade chamada Setting
que é compartilhada entre threads.
Uma nova instância ocasionalmente será atribuída a essa variável por outras threads.
O método que usa Setting
copiará a referência antes de funcionar, e não há problema em usar a antiga Setting
se ela estiver sendo atualizada quando o trabalho estiver prestes a começar, então o bloqueio não é necessário.
A leitura e a escrita únicas do tipo de referência são atômicas em C#, mas instruções compostas como var foo = new Foo()
essa não são atômicas e podem ser reordenadas.
Este problema é bem ilustrado no bloqueio de dupla verificação.
https://medium.com/@wangberlin2000/why-volatile-is-essential-in-double-checked-locking-singleton-0ba2906623fe
O link foi escrito em Java, mas o mesmo se aplica a C#.
https://csharpindepth.com/Articles/Singleton
Adicionado também o link para C#. Confira a "Terceira versão - tentativa de segurança de thread usando bloqueio de verificação dupla".
Gostaria de saber se é possível ler o Setting
quando seu endereço de memória é alocado, mas a inicialização do object
não é concluída.
O código de exemplo tem 3 Setting
variáveis de membro de instância, property
e virtual property
funções que atualizam o Setting
. É para fins de demonstração e o código real tem apenas uma Setting
e uma função.
- É
UpdateMemberAsync
seguro usar, já que o conteúdoGetSettingAsync
não pode vazar antes ou depois daawait
reordenação da memória? - Não é
UpdateMemberByInlinable
seguro usar porqueGetSetting
pode ser incorporado em vez de retornar o inicializadoobject
? - É
UpdateVirtualMember
seguro usar porquevirtual property
não pode ser embutido? - É
UpdateMemberByNonInlinable
seguro usar porqueApi
não pode ser embutido? - É
UpdateMemberUsingBarrier
seguro usar? - Há algo que estou esquecendo além dos exemplos fornecidos?
Adicionado o link fornecido por Peter Cordes para aqueles que buscam informações mais detalhadas. https://preshing.com/20120612/an-introduction-to-lock-free-programming/
public class Service
{
public Setting Setting = new();
public Setting SettingProperty { get; set; } = new();
public virtual Setting SettingVirtualProperty { get; set; } = new();
// Called by other threads.
public async Task StartAsync()
{
// It doesn't have to be the most recent value.
var setting = Setting;
await Foo(setting);
await Bar(setting);
}
// Api is a class with a non-virtual or sealed function.
public void UpdateMemberByInlinable(Api api)
{
Setting = api.GetSetting();
// Inlined as below.
var t = new Setting();
Setting = ptrT;
// or
SettingProperty.BackingField = ptrT;
// Set members of t after assigning the ptr.
}
// Api can be anything.
public async Task UpdateMemberAsync(Api api)
{
Setting = await api.GetSettingAsync();
// or
SettingProperty = await api.GetSettingAsync();
// or
SettingVirtualProperty = await api.GetSettingAsync();
}
// Api can be anything.
public void UpdateVirtualMember(Api api)
{
SettingVirtualProperty = api.GetSetting();
}
// Api is an interface or a class with a non-sealed virtual function.
public void UpdateMemberByNonInlinable(Api api)
{
Setting = api.GetSetting();
// or
SettingProperty = api.GetSetting();
}
// Api can be anything.
public void UpdateMemberUsingBarrier(Api api)
{
var setting = api.GetSetting();
Thread.MemoryBarrier();
Setting = setting;
// or
SettingProperty = setting;
}
}
Sim, teoricamente é possível. O compilador C# tem permissão para fazer todos os tipos de reordenações que não afetam a execução em uma única thread. Se isso é praticamente possível, ou seja, se os compiladores C# existentes realmente realizam tais otimizações, é uma questão que alguns especialistas podem ser capazes de responder, mas eu não sou um deles.
Nenhuma delas é segura para uso. Todas são inseguras. Até mesmo a
UpdateMemberUsingBarrier
é insegura, pois não há barreira de memória ao ler o campo compartilhado. A ausência dessa barreira torna possível, pelo menos teoricamente, que instruções após a leitura sejam reordenadas e saltem antes da leitura.A maneira padrão de evitar o cenário de leitura de instâncias parcialmente inicializadas é a
volatile
palavra-chave . Basta anotar seuSetting
campo comovolatile
, e o C# adicionará as barreiras de memória apropriadas sempre que você ler ou atualizar o campo. Você pode aprender sobre as reordenações específicas que são evitadas lendo a documentação dos métodosVolatile.Read
eVolatile.Write
. Você também pode consultar um exemplo de uso educacional davolatile
palavra-chave que publiquei nesta resposta .A atribuição de uma referência é sempre atômica, portanto, nunca pode haver problemas com gravações distorcidas de uma referência. Isso pode ser um problema apenas em tipos de valor maiores que o tamanho do ponteiro nativo. Portanto, se
Setting
for uma struct, você pode ter um problema; se for uma classe, tudo bem.Eu esperaria que o compilador fosse bastante restrito em relação à otimização em torno de um await.
async
Os métodos serão essencialmente reescritos em uma máquina de estados, e você precisa considerar quaisquer otimizações no contexto dessa máquina de estados. Observe que aawait
intenção é ser uma API fácil de usar para métodos assíncronos, exigindo que o usuário insira quaisquer barreiras que contrariem esse objetivo.Claro, mesmo que todas as operações sejam atômicas, isso não significa necessariamente que todo o programa seja thread-safe. Você ainda pode precisar de bloqueios para garantir que sequências de operações não sejam executadas simultaneamente. Mas não há exemplos disso no código publicado, até onde eu sei.