Novamente verifiquei o bloqueio e C #
-
27-10-2019 - |
Pergunta
Recentemente, tenho refatorado parte do meu código C # e descobri que algumas práticas de bloqueio foram verificadas novamente. Eu não sabia que era uma prática ruim na época e eu realmente quero me livrar disso.
O problema é que eu tenho uma classe que deve ser inicializada lentamente e freqüentemente acessada por vários threads. Eu também não quero mover a inicialização para um inicializador estático, porque estou planejando usar uma referência fraca para evitar que o objeto inicializado permaneça muito tempo na memória. No entanto, se necessário, quero 'reviver' o objeto garantindo que isso aconteça de maneira segura para thread.
Eu queria saber se usar um ReaderWriterLockSlim em C # e inserir um UpgradeableReadLock antes da primeira verificação e, se necessário, inserir um bloqueio de gravação para a inicialização seria uma solução aceitável. Aqui está o que estou tendo em mente:
public class LazyInitialized
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private volatile WeakReference _valueReference = new WeakReference(null);
public MyType Value
{
get
{
MyType value = _valueReference.Target as MyType;
_lock.EnterUpgradeableReadLock();
try
{
if (!_valueReference.IsAlive) // needs initializing
{
_lock.EnterWriteLock();
try
{
if (!_valueReference.IsAlive) // check again
{
// prevent reading the old weak reference
Thread.MemoryBarrier();
_valueReference = new WeakReference(value = InitializeMyType());
}
}
finally
{
_lock.ExitWriteLock();
}
}
}
finally
{
_lock.ExitUpgradeableReadLock();
}
return value;
}
}
private MyType InitializeMyType()
{
// code not shown
}
}
Meu ponto é que nenhum outro thread deve tentar inicializar o item mais uma vez, enquanto muitos threads devem ler simultaneamente uma vez que o valor é inicializado. O bloqueio de leitura atualizável deve bloquear todos os leitores se o bloqueio de gravação for adquirido, portanto, enquanto o objeto está sendo inicializado, o comportamento será semelhante a ter uma instrução de bloqueio onde o bloqueio de leitura atualizável começa. Após a inicialização, o bloqueio de leitura atualizável permitirá vários encadeamentos, portanto, o desempenho de espera de cada encadeamento não estará presente.
Eu também li um artigo aqui dizendo que volátil faz com que barreiras de memória sejam inseridas automaticamente antes da leitura e depois da gravação, portanto, presumo que apenas uma barreira definida manualmente entre a leitura e a gravação será suficiente para garantir que o objeto _valueReference seja lido corretamente. Terei prazer em receber seus conselhos e críticas por usar essa abordagem.
Solução
Para enfatizar o ponto que @Mannimarco faz: se este é o único ponto de acesso ao Value, e parece que sim, então toda a configuração do ReaderWriterLockSlim não é melhor do que uma abordagem simples de Monitor.Enter / Monitor.Leave.Porém, é muito mais complicado.
Portanto, acredito que o código a seguir é equivalente em função e eficiência:
private WeakReference _valueReference = new WeakReference(null);
private object _locker = new object();
public MyType Value
{
get
{
lock(_locker) // also provides the barriers
{
value = _valueReference.Target;
if (!_valueReference.IsAlive)
{
_valueReference = new WeakReference(value = InitializeMyType());
}
return value;
}
}
}
Outras dicas
Um aviso: apenas um único thread pode entrar no modo UpgradeableReadLock por vez.Verifique ReaderWriterLockSlim .Portanto, se os threads se acumulam enquanto o primeiro thread entra no modo de gravação e cria o objeto, você terá um gargalo até que o backup seja (com sorte) resolvido.Eu sugeriria seriamente o uso de um inicializador estático, isso tornará sua vida mais fácil.
EDIT: Dependendo da frequência com que o objeto precisa ser recriado, eu realmente sugeriria o uso da classe Monitor e seus métodos Wait e Pulse.Se o valor precisar ser recriado, faça com que os threads Esperem em um objeto e Pulse outro objeto para permitir que um thread de trabalho saiba que precisa ser ativado e criar um novo objeto.Uma vez que o objeto foi criado, PulseAll permitirá que todos os threads do leitor sejam ativados e capturem o novo valor.(em teoria)