.net Core AsyncLocal verliert seinen Wert

Nov 13 2020

Ich verwende ein ähnliches Muster wie HttpContextAccessor

Die vereinfachte Version ist wie folgt, Console.WriteLine(SimpleStringHolder.StringValue)soll nicht null sein.

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

Ruft im obigen Code OutterAsynceine asynchrone Methode auf InnerAsync, in InnerAsyncder StringValuegesetzt ist, wodurch AsyncLocal seinen Kontext verliert, in dem OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);null ist.

Ich denke, die Magie liegt im Eigenschaftssatz von SimpleStringHolder. Wenn Sie den folgenden Code entfernen, werden die Dinge richtig.

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

Der obige Code funktioniert wie erwartet.

Bitte helfen Sie mir zu verstehen, was Zauberei ist?

Antworten

3 PeterDuniho Nov 13 2020 at 12:43

AsyncLocal<T>existiert, um einen Mechanismus zum Beibehalten von Werten in einem asynchronen Ausführungskontext bereitzustellen. Der Schlüssel dazu sind zwei Faktoren, die in Ihrem Beispiel eine Rolle spielen:

  1. Mit awaitkann eine Methode zum Aufrufer zurückkehren, wodurch sich der Kontext ändern kann. ThreadLocal<T>Wenn beim älteren Typ die Ausführung die Kontrolle an die Methode zurückgibt, kann sie sich in einem anderen Thread befinden, obwohl asyncder Kontext aus der Sicht derselbe ist. Mit using wird AsyncLocal<T>sichergestellt, dass der Status des Kontexts wiederhergestellt wird, wenn das awaitSteuerelement nach Abschluss des erwarteten Objekts an die Methode zurückgegeben wird.
  2. Bis etwas passiert, bei dem sich der Kontext ändern müsste, ist der aktuelle Status eines AsyncLocal<T>Objekts der vorherige. Das heißt, eine Methode erbt im Wesentlichen den Zustand, in dem sich das Objekt befand, als es aufgerufen wurde. Wenn Sie es mit einfachen Werten zu tun haben, lauern keine Überraschungen, aber bei einem Referenztyp wie Ihrem ValueHolderTyp ist das einzige, was Sie AsyncLocal<T>verfolgen, die Referenz auf dieses Objekt. Es gibt immer noch nur eine Kopie des Objekts und Änderungen am Status einer bestimmten Objektarbeit, wie sie es immer mit oder ohne herumschwebende asynchrone Kontexte tun (dh sie werden durch einen Verweis auf dieses Objekt gesehen).

In dem von Ihnen angegebenen Codebeispiel:

  1. OutterAsync()Setzt die StringValueEigenschaft auf "1", wodurch ein neues ValueHolderObjekt erstellt wird, und die StringValueEigenschaft dieses Objekts auf "1".
  2. OutterAsync()Anrufe InnerAsync(). Diese Methode ruft dann die stringReferenz vom Inhaber ab (indirekt… dh durch Durchlaufen der SimpleStringHolder.StringValueEigenschaft). Da zu diesem Zeitpunkt weder Änderungen am Wert noch am Kontext vorgenommen wurden, ValueHolderwird in diesem Fall dasselbe Objekt verwendet, sodass Sie "1"zurückkehren.
  3. InnerAsync()wartet auf eine asynchrone Aufgabe, wodurch ein neuer Ausführungskontext erstellt wird, um Änderungen am AsyncValue<T>Objekt in diesem Kontext zu isolieren . Ab diesem Zeitpunkt werden Änderungen am Objekt vom Code nicht mehr in einem anderen Kontext angezeigt. Zum Beispiel der Code, der in der OutterAsync()Methode ausgeführt wird.
  4. Nachdem die asynchrone Aufgabe abgeschlossen InnerAsync()ist, legt diese Methode einen neuen Wert für die SimpleStringHolder.StringValueEigenschaft fest. Da der frühere Kontext geerbt wurde, setzt der Setter holder.StringValuebeim nullFestlegen auf die Eigenschaft des Objekts, in dem er erstellt wurde OutterAsync(). Aber… weil sich der Code in einem neuen Kontext befindet, CurrentHolder.Valuewird diese Änderung in diesem Kontext isoliert , wenn der Setter der Eigenschaft dann einen neuen Wert zuweist .
  5. Wenn die InnerAsync()Methode endgültig abgeschlossen ist, ist die Aufgabe abgeschlossen, auf die die OutterAsync()Methode awaitgewartet hat. Dadurch wird der AsyncValue<T>Status der OutterAsync()Methode im Kontext der Methode wiederhergestellt , der sich von dem Kontext unterscheidet, in dem InnerAsync()der SimpleStringHolder.StringValueWert aktualisiert wurde . Insbesondere ist dieser wiederhergestellte Status ein Verweis auf das ValueHolderObjekt, das ursprünglich festgelegt wurde, SimpleStringHolderals die holder.StringValueEigenschaft auf null gesetzt wurde.
  6. Wenn Sie sich OutterAsync()dann den Eigenschaftswert ansehen, wird er auf null gesetzt. Weil es auf null gesetzt wurde.

In Ihrem eigenen Experimentieren, können Sie entweder die Nullzuordnung löschen ganz oder einfach weglassen , die Abtretung SimpleStringHolder.StringValuenach der InnerAsync()‚s awaitAussage (weil , wenn Sie nicht die Zuordnung machen, dann die Null Zuordnung nie ausgeführt wird). In beiden Fällen findet die Nullzuweisung nicht statt, sodass der zuvor zugewiesene Wert erhalten bleibt.

Aber wenn Sie die Null Zuordnung vornehmen zu tun, der Anrufer OutterAsync()wird seinen Kontext haben , wieder hergestellt und haben den Objekthalter Referenz dann wiederhergestellt, und das eigene des Inhabers Objekt stringBezug hatte bereits Satz null, so dass das , was OutterAsync()sieht.

Verwandte
Themen : Wie wirkt sich AsyncLocal auf nicht asynchronen / erwarteten Code aus?
Warum gibt AsyncLocal unterschiedliche Ergebnisse zurück, wenn der Code geringfügig überarbeitet wird?
Tut AsyncLocalauch die Dinge, ThreadLocaldie tun ?