.net core AsyncLocal perd sa valeur

Nov 13 2020

J'utilise un modèle similaire à HttpContextAccessor

La version simplifiée est la suivante, Console.WriteLine(SimpleStringHolder.StringValue)n'est pas censée être nulle.

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
    }
}

Dans le code ci - dessus, OutterAsyncappelle une méthode asynchrone InnerAsync, dans InnerAsyncl' StringValueest réglée, ce qui rend AsyncLocal perd son contexte OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);est nulle.

Je pense que la magie est dans l'ensemble de propriétés de SimpleStringHolder, la suppression du code suivant arrangera les choses.

if (holder != null)
{
    holder.StringValue = null;
}

Le code ci-dessus fonctionne comme prévu.

S'il vous plaît, aidez-moi à comprendre de quoi s'agit-il?

Réponses

3 PeterDuniho Nov 13 2020 at 12:43

AsyncLocal<T>existe pour fournir un mécanisme permettant de conserver les valeurs dans un contexte d'exécution asynchrone. La clé de ceci est deux facteurs impliqués dans votre exemple:

  1. An awaitpermet à une méthode de revenir à l'appelant, ce qui pourrait changer le contexte. Avec l'ancien ThreadLocal<T>type, lorsque l'exécution renvoie le contrôle à la méthode, elle peut être dans un thread différent, même si du asyncpoint de vue le contexte est le même. L'utilisation AsyncLocal<T>garantit que l'état du contexte est restauré lorsque le awaitretourne le contrôle à la méthode une fois que l'objet attendu est terminé.
  2. Jusqu'à ce que quelque chose se produise qui exigerait que le contexte change, l'état actuel d'un AsyncLocal<T>objet est ce qu'il était auparavant. C'est-à-dire qu'une méthode hérite essentiellement de l'état dans lequel se trouvait l'objet lorsqu'il a été appelé. Si vous avez affaire à des valeurs simples, aucune surprise ne se cache, mais dans le cas d'un type de référence comme votre ValueHoldertype, la seule chose qui AsyncLocal<T>garde une trace est la référence à cet objet. Il n'y a toujours qu'une seule copie de l'objet, et les changements d'état de tout objet donné fonctionnent comme ils le font toujours avec ou sans contextes asynchrones flottant (c'est-à-dire qu'ils sont vus par toute référence à cet objet).

Donc, dans l'exemple de code que vous avez fourni:

  1. OutterAsync()définit la StringValuepropriété sur "1", ce qui entraîne la création d'un nouvel ValueHolderobjet et la StringValuepropriété de cet objet définie sur "1".
  2. OutterAsync()appels InnerAsync(). Cette méthode récupère ensuite la stringréférence du détenteur (indirectement… c'est-à-dire en passant par la SimpleStringHolder.StringValuepropriété). Comme aucune modification de la valeur ni du contexte n'a été effectuée à ce stade, le même ValueHolderobjet est utilisé dans ce cas, vous "1"revenez donc.
  3. InnerAsync()attend une tâche asynchrone, ce qui entraîne la création d'un nouveau contexte d'exécution dans le but d'isoler les modifications apportées à l' AsyncValue<T>objet dans ce contexte. À partir de ce moment, les modifications apportées à l'objet ne sont pas vues par le code dans un contexte différent. Par exemple, le code s'exécutant dans la OutterAsync()méthode.
  4. Une fois la tâche asynchrone terminée InnerAsync(), cette méthode définit une nouvelle valeur pour la SimpleStringHolder.StringValuepropriété. Étant donné que le contexte antérieur a été hérité, lorsque le paramètre définit holder.StringValuesur null, il définit la propriété de l'objet qui a été créé dans OutterAsync(). Mais… parce que le code est dans un nouveau contexte, lorsque le setter attribue ensuite une nouvelle valeur à la CurrentHolder.Valuepropriété, ce changement est isolé dans ce contexte.
  5. Lorsque la InnerAsync()méthode est enfin terminée, cela termine la tâche que la OutterAsync()méthode awaitattendait. Cela oblige le AsyncValue<T>à restaurer son état dans le OutterAsync()contexte de la méthode, qui est différent du contexte dans InnerAsync()lequel il a mis à jour la SimpleStringHolder.StringValuevaleur. Et plus précisément, cet état restauré est une référence à l' ValueHolderobjet qui a été initialement défini dans le SimpleStringHolderlorsque la holder.StringValuepropriété a été définie sur null.
  6. Ainsi, lorsqu'il OutterAsync()examine ensuite la valeur de la propriété, il la trouve définie sur null. Parce qu'il a été défini sur null.

Dans votre propre test, vous pouvez soit supprimer complètement l'affectation nulle, soit simplement omettre l'affectation SimpleStringHolder.StringValueaprès l' instruction InnerAsync()s await(car si vous n'effectuez pas l'affectation, l'affectation nulle n'est jamais exécutée). Dans tous les cas, l'affectation nulle ne se produit pas et la valeur précédemment attribuée reste donc.

Mais si vous effectuez une affectation nulle, l'appelant OutterAsync()va avoir son contexte restauré, et la référence d'objet détenteur ensuite restaurée, et la propre stringréférence de cet objet détenteur a déjà été définie sur null, c'est donc ce que OutterAsync()voit.

Lecture connexe:
Quel est l'effet d'AsyncLocal dans le code non async / d'attente?
Pourquoi AsyncLocal renvoie-t-il des résultats différents lorsque le code est légèrement remanié?
Fait-il AsyncLocalaussi les choses qui le ThreadLocalfont?