Na documentação do ConcurrentDictionary é declarado que a fábrica de valores é chamada fora dos bloqueios e que uma chave/valor pode ser inserido por outro thread enquanto valueFactory está gerando um valor. Você não pode confiar que só porque valueFactory foi executado, seu valor produzido será inserido no dicionário e retornado.
Meu código original se parece com isso:
public class MyClass
{
private readonly ConcurrentDictionary<string, Task<string>> myData = new();
public Task<string> GetData(string key)
{
return myData.GetOrAdd(key, key => longCalculation(key));
}
}
Se várias threads chamarem MyClass.GetData("theSameKey")
ao mesmo tempo, longCalculation
"theSameKey" será executado várias vezes porque valueFactory
será delegado a outra string.
Para evitar ter que longCalculation
executar desnecessariamente, alterei meu código para usar Lazy, conforme sugerido por JG no SD, para que o delegado fosse executado apenas uma vez:
public class MyClass
{
private readonly ConcurrentDictionary<string, Lazy<Task<string>>> myData = new();
public async Task<string> GetData(string key)
{
var lazyTask = new new Lazy<Task<string>>(() => longCalculation(key),
LazyThreadSafetyMode.ExecutionAndPublication);
var task = myData.GetOrAdd(key, lazyTask).Value;
return await task;
}
}
Esta versão, se houver várias chamadas de threads, MyClass.GetData("theSameKey")
longCalculation
é executada apenas uma vez. Embora eu tenha uma correção para obter o comportamento desejado, gostaria de ter mais detalhes explicando por que isso acontece.
Vamos supor que duas threads estejam competindo para adicionar a mesma chave no
ConcurrentDictionary
. Ambas as threads invocam ovalueFactory
, mas apenas uma conseguirá inserir o valor no dicionário. No entanto, ambas as threads receberão o mesmo valor como resultado doGetOrAdd
. Tanto a thread que venceu a corrida quanto a thread que a perdeu receberão o valor produzido pela thread que venceu a corrida. O outro valor, aquele produzido pela thread que perdeu a corrida, será descartado. Ele não será inserido no dicionário e, como ninguém possui uma referência a ele, será eventualmente reciclado pelo coletor de lixo.No seu caso, o valor produzido por
valueFactory
é aLazy<Task<string>>
. Isso representa uma operação invocada preguiçosamente sob demanda, quando alguém lê aValue
propriedade daLazy<T>
instância. Se ninguém ler aLazy<T>.Value
, a operação nunca será invocada. O que é exatamente o que acontece com a ,Lazy<T>
produzida pela thread que perdeu a corrida. Ninguém jamais lerá suaValue
, então alongCalculation
associada a estaLazy<T>
instância nunca será invocada. Somente a ,Lazy<T>
produzida pela thread vencedora, é exposta ao mundo externo, o que garante que a ,longCalculation
será invocada no máximo uma vez para cada chave no dicionário.