.net core AsyncLocal perde il suo valore
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
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:
- An
awaitconsente a un metodo di tornare al chiamante, che potrebbe cambiare il contesto. Con il vecchioThreadLocal<T>tipo, quando l'esecuzione restituisce il controllo al metodo, potrebbe essere in un thread diverso, anche se dalasyncpunto di vista il contesto è lo stesso. L'utilizzoAsyncLocal<T>assicura che lo stato del contesto venga ripristinato quando ilawaitcontrollo restituisce il controllo al metodo dopo il completamento dell'oggetto in attesa. - 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 tuoValueHoldertipo, l'unica cosa di cuiAsyncLocal<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:
OutterAsync()imposta laStringValueproprietà su"1", il che si traduce nellaValueHoldercreazione di un nuovo oggetto e nellaStringValueproprietà di tale oggetto impostata su"1".OutterAsync()chiamateInnerAsync(). Questo metodo recupera quindi ilstringriferimento dal titolare (indirettamente ... cioè passando attraverso laSimpleStringHolder.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.InnerAsync()attende un'attività asincrona, che provoca la creazione di un nuovo contesto di esecuzione allo scopo di isolare le modifiche apportateAsyncValue<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 nelOutterAsync()metodo.- Dopo il completamento dell'attività asincrona
InnerAsync(), tale metodo imposta un nuovo valore per laSimpleStringHolder.StringValueproprietà. Poiché il contesto precedente è stato ereditato, quando il setter impostaholder.StringValuesunull, imposta la proprietà dell'oggetto in cui è stato creatoOutterAsync(). Ma ... poiché il codice si trova in un nuovo contesto, quando il setter assegna quindi un nuovo valore allaCurrentHolder.Valueproprietà, tale modifica viene isolata in quel contesto. - Quando il
InnerAsync()metodo alla fine viene completato, questo completa l'attività che ilOutterAsync()metodoawaitstava aspettando. Ciò fa sìAsyncValue<T>che ripristini il suo stato nelOutterAsync()contesto del metodo, che è diverso dal contesto inInnerAsync()cui si trovava quando ha aggiornato ilSimpleStringHolder.StringValuevalore. E in particolare, questo stato ripristinato è un riferimentoValueHolderall'oggetto originariamente impostato nelSimpleStringHoldermomento in cui laholder.StringValueproprietà è stata impostata su null. - 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?