.net core AsyncLocal perde il suo valore

Nov 13 2020

Uso un modello simile a HttpContextAccessor

La versione semplificata è la seguente, Console.WriteLine(SimpleStringHolder.StringValue)non dovrebbe essere nulla.

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

Nel codice precedente, OutterAsyncrichiama un metodo asincrono InnerAsync, in InnerAsyncla StringValueè impostato, il che rende AsyncLocal perde il suo contesto OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);è nullo.

Penso che la magia sia nel set di proprietà del SimpleStringHolder, la rimozione del codice seguente renderà le cose giuste.

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

Il codice precedente funziona come previsto.

Per favore aiutami a capire che cos'è questa stregoneria?

Risposte

3 PeterDuniho Nov 13 2020 at 12:43

AsyncLocal<T>esiste per fornire un meccanismo per preservare i valori all'interno di un contesto di esecuzione asincrono. La chiave per questo sono due fattori coinvolti nel tuo esempio:

  1. An awaitconsente a un metodo di tornare al chiamante, che potrebbe cambiare il contesto. Con il vecchio ThreadLocal<T>tipo, quando l'esecuzione restituisce il controllo al metodo, potrebbe essere in un thread diverso, anche se dal asyncpunto di vista il contesto è lo stesso. L'utilizzo AsyncLocal<T>assicura che lo stato del contesto venga ripristinato quando il awaitcontrollo restituisce il controllo al metodo dopo il completamento dell'oggetto in attesa.
  2. Finché non accade qualcosa che richiederebbe il cambiamento del contesto, lo stato corrente di un AsyncLocal<T>oggetto è quello che era in precedenza. Ad esempio, un metodo eredita essenzialmente lo stato in cui si trovava l'oggetto quando è stato chiamato. Se hai a che fare con valori semplici, non si nascondono sorprese, ma nel caso di un tipo di riferimento come il tuo ValueHoldertipo, l'unica cosa di cui AsyncLocal<T>tiene traccia è il riferimento a quell'oggetto. Esiste ancora una sola copia dell'oggetto, e le modifiche allo stato di ogni dato oggetto di questo tipo funzionano come fanno sempre con o senza contesti asincroni che fluttuano (cioè sono visti da qualsiasi riferimento a quell'oggetto).

Quindi, nell'esempio di codice che hai fornito:

  1. OutterAsync()imposta la StringValueproprietà su "1", il che si traduce nella ValueHoldercreazione di un nuovo oggetto e nella StringValueproprietà di tale oggetto impostata su "1".
  2. OutterAsync()chiamate InnerAsync(). Questo metodo recupera quindi il stringriferimento dal titolare (indirettamente ... cioè passando attraverso la SimpleStringHolder.StringValueproprietà). Poiché a questo punto non sono state apportate modifiche al valore né al contesto, ValueHolderin questo caso viene utilizzato lo stesso oggetto, quindi "1"torni indietro.
  3. InnerAsync()attende un'attività asincrona, che provoca la creazione di un nuovo contesto di esecuzione allo scopo di isolare le modifiche apportate AsyncValue<T>all'oggetto in quel contesto. Da questo punto in poi, le modifiche all'oggetto non vengono visualizzate dal codice in un contesto diverso. Ad esempio, il codice in esecuzione nel OutterAsync()metodo.
  4. Dopo il completamento dell'attività asincrona InnerAsync(), tale metodo imposta un nuovo valore per la SimpleStringHolder.StringValueproprietà. Poiché il contesto precedente è stato ereditato, quando il setter imposta holder.StringValuesu null, imposta la proprietà dell'oggetto in cui è stato creato OutterAsync(). Ma ... poiché il codice si trova in un nuovo contesto, quando il setter assegna quindi un nuovo valore alla CurrentHolder.Valueproprietà, tale modifica viene isolata in quel contesto.
  5. Quando il InnerAsync()metodo alla fine viene completato, questo completa l'attività che il OutterAsync()metodo awaitstava aspettando. Ciò fa sì AsyncValue<T>che ripristini il suo stato nel OutterAsync()contesto del metodo, che è diverso dal contesto in InnerAsync()cui si trovava quando ha aggiornato il SimpleStringHolder.StringValuevalore. E in particolare, questo stato ripristinato è un riferimento ValueHolderall'oggetto originariamente impostato nel SimpleStringHoldermomento in cui la holder.StringValueproprietà è stata impostata su null.
  6. Quindi, quando OutterAsync()poi va a guardare il valore della proprietà, lo trova impostato su null. Perché era impostato su null.

Nella tua sperimentazione, puoi rimuovere del tutto l'assegnazione nulla o semplicemente omettere l'assegnazione SimpleStringHolder.StringValuedopo l' istruzione InnerAsync()s await(perché se non fai l'assegnazione, l'assegnazione nulla non viene mai eseguita). In ogni caso, l'assegnazione null non viene eseguita e quindi il valore assegnato in precedenza rimane.

Ma se si effettua l'assegnazione null, il chiamante OutterAsync()avrà ripristinato il suo contesto e verrà ripristinato il riferimento all'oggetto contenitore e il riferimento a quell'oggetto contenitore stringera già stato impostato null, quindi questo è ciò che OutterAsync()vede.

Lettura correlata:
qual è l'effetto di AsyncLocal in codice non asincrono / in attesa?
Perché AsyncLocal restituisce risultati diversi quando il codice viene leggermente riformulato?
Fa AsyncLocalanche le cose che ThreadLocalfa?