.net core AsyncLocal perde seu valor
Eu uso um padrão semelhante ao HttpContextAccessor
A versão simplificada é a seguinte, Console.WriteLine(SimpleStringHolder.StringValue)não deve ser nula.
public class SimpleStringHolder
{
private static readonly AsyncLocal<ValueHolder> CurrentHolder = new AsyncLocal<ValueHolder>();
public static string StringValue
{
get => CurrentHolder.Value?.StringValue;
set
{
var holder = CurrentHolder.Value;
if (holder != null)
{
holder.StringValue = null;
}
if (value != null)
{
CurrentHolder.Value = new ValueHolder() { StringValue = value };
}
}
}
private class ValueHolder
{
public string StringValue;
}
}
class Program
{
private static readonly AsyncLocal<string> currentValue = new AsyncLocal<string>();
public static void Main(string[] args)
{
var task = Task.Run(async () => await OutterAsync());
task.Wait();
}
public static async Task OutterAsync()
{
SimpleStringHolder.StringValue = "1";
await InnerAsync();
Console.WriteLine(SimpleStringHolder.StringValue); //##### the value is gone ######
}
public static async Task InnerAsync()
{
var lastValue = SimpleStringHolder.StringValue;
await Task.Delay(1).ConfigureAwait(false);
SimpleStringHolder.StringValue = lastValue; // comment this line will make it work
Console.WriteLine(SimpleStringHolder.StringValue); //the value is still here
}
}
No código acima, OutterAsyncinvoca um método assíncrono InnerAsync, em InnerAsynco StringValueé definido, o que torna AsyncLocal perde o seu contexto na OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);é nula.
Acho que a mágica está no conjunto de propriedades de SimpleStringHolder, remover o código a seguir fará as coisas certas.
if (holder != null)
{
holder.StringValue = null;
}
O código acima funciona conforme o esperado.
Por favor, me ajude a entender que feitiçaria é essa?
Respostas
AsyncLocal<T>existe para fornecer um mecanismo para preservar valores em um contexto de execução assíncrona. A chave para isso são dois fatores envolvidos em seu exemplo:
- Um
awaitpermite que um método retorne ao chamador, o que pode alterar o contexto. Com oThreadLocal<T>tipo mais antigo , quando a execução retorna o controle para o método, pode ser em uma thread diferente, embora doasyncponto de vista o contexto seja o mesmo. O usoAsyncLocal<T>garante que o estado do contexto seja restaurado quando oawaitcontrole retorna ao método após a conclusão do objeto aguardável. - Até que aconteça algo que exigiria a mudança do contexto, o estado atual de um
AsyncLocal<T>objeto é o que era antes. Ou seja, um método herda essencialmente o estado em que o objeto estava quando foi chamado. Se você está lidando com valores simples, nenhuma surpresa se esconde, mas, no caso de um tipo de referência como o seuValueHolder, a única coisa queAsyncLocal<T>acompanha é a referência a esse objeto. Ainda há apenas uma cópia do objeto, e as alterações no estado de qualquer determinado objeto funcionam como sempre fazem com ou sem contextos assíncronos flutuando (ou seja, são vistos por qualquer referência a esse objeto).
Portanto, no exemplo de código que você forneceu:
OutterAsync()define aStringValuepropriedade como"1", o que resulta na criação de um novoValueHolderobjeto e naStringValuepropriedade desse objeto sendo definida como"1".OutterAsync()chamadasInnerAsync(). Esse método, então, recupera astringreferência do titular (indiretamente ... ou seja, passando pelaSimpleStringHolder.StringValuepropriedade). Uma vez que nenhuma alteração no valor ou contexto foi feita neste ponto, o mesmoValueHolderobjeto é usado neste caso, então você"1"volta.InnerAsync()aguarda uma tarefa assíncrona, o que faz com que um novo contexto de execução seja criado com o objetivo de isolar as alterações feitas noAsyncValue<T>objeto para esse contexto. Deste ponto em diante, as alterações no objeto não são vistas pelo código em um contexto diferente. Por exemplo, o código em execução noOutterAsync()método.- Após a conclusão da tarefa assíncrona em
InnerAsync(), esse método define um novo valor para aSimpleStringHolder.StringValuepropriedade. Como o contexto anterior foi herdado, quando o configurador é definidoholder.StringValuecomonull, ele está definindo a propriedade do objeto que foi criado emOutterAsync(). Mas ... como o código está em um novo contexto, quando o configurador atribui um novo valor àCurrentHolder.Valuepropriedade, essa alteração é isolada nesse contexto. - Quando o
InnerAsync()método finalmente é concluído, a tarefa que oOutterAsync()métodoawaitestava aguardando é concluída . Isso faz com que oAsyncValue<T>restaure seu estado para oOutterAsync()contexto do método, que é diferente do contexto que estavaInnerAsync()quando ele atualizou oSimpleStringHolder.StringValuevalor. E, especificamente, esse estado restaurado é uma referência aoValueHolderobjeto que foi originalmente definido noSimpleStringHolderquando aholder.StringValuepropriedade foi definida como null. - Então, quando
OutterAsync()for examinar o valor da propriedade, ele o encontrará definido como nulo. Porque foi definido como nulo.
Em sua própria experimentação, você pode remover a atribuição nula por completo, ou simplesmente omitir a atribuição para SimpleStringHolder.StringValuedepois do InnerAsync()'s awaitdeclaração (porque se você não fizer a atribuição, em seguida, a atribuição nulo nunca é executado). De qualquer forma, a atribuição nula não acontece e, portanto, o valor atribuído anteriormente permanece.
Mas se você fizer a atribuição nula, o chamador OutterAsync()terá seu contexto restaurado e a referência do objeto titular então restaurada, e a própria stringreferência desse objeto titular já foi definida para null, então é isso que OutterAsync()vê.
Leitura relacionada:
Qual é o efeito de AsyncLocal em código não async / await?
Por que AsyncLocal retorna resultados diferentes quando o código é ligeiramente refatorado?
Será AsyncLocaltambém fazer as coisas que ThreadLocalfaz?