.net core AsyncLocal perde seu valor

Nov 13 2020

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

3 PeterDuniho Nov 13 2020 at 12:43

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:

  1. Um awaitpermite que um método retorne ao chamador, o que pode alterar o contexto. Com o ThreadLocal<T>tipo mais antigo , quando a execução retorna o controle para o método, pode ser em uma thread diferente, embora do asyncponto de vista o contexto seja o mesmo. O uso AsyncLocal<T>garante que o estado do contexto seja restaurado quando o awaitcontrole retorna ao método após a conclusão do objeto aguardável.
  2. 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 seu ValueHolder, a única coisa que AsyncLocal<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:

  1. OutterAsync()define a StringValuepropriedade como "1", o que resulta na criação de um novo ValueHolderobjeto e na StringValuepropriedade desse objeto sendo definida como "1".
  2. OutterAsync()chamadas InnerAsync(). Esse método, então, recupera a stringreferência do titular (indiretamente ... ou seja, passando pela SimpleStringHolder.StringValuepropriedade). Uma vez que nenhuma alteração no valor ou contexto foi feita neste ponto, o mesmo ValueHolderobjeto é usado neste caso, então você "1"volta.
  3. 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 no AsyncValue<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 no OutterAsync()método.
  4. Após a conclusão da tarefa assíncrona em InnerAsync(), esse método define um novo valor para a SimpleStringHolder.StringValuepropriedade. Como o contexto anterior foi herdado, quando o configurador é definido holder.StringValuecomo null, ele está definindo a propriedade do objeto que foi criado em OutterAsync(). 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.
  5. Quando o InnerAsync()método finalmente é concluído, a tarefa que o OutterAsync()método awaitestava aguardando é concluída . Isso faz com que o AsyncValue<T>restaure seu estado para o OutterAsync()contexto do método, que é diferente do contexto que estava InnerAsync()quando ele atualizou o SimpleStringHolder.StringValuevalor. E, especificamente, esse estado restaurado é uma referência ao ValueHolderobjeto que foi originalmente definido no SimpleStringHolderquando a holder.StringValuepropriedade foi definida como null.
  6. 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?