Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Quando si lavora con async e await, due tipi di contesto svolgono ruoli importanti ma molto diversi: ExecutionContext e SynchronizationContext. Si apprenderà cosa fa ognuno di essi, come ognuno interagisce con async/awaite perché SynchronizationContext.Current non scorre attraverso i punti await.
Che cos'è ExecutionContext?
ExecutionContext è un contenitore per lo stato di ambiente che scorre con il flusso di controllo logico del programma. In un mondo sincrono, le informazioni di ambiente si trovano nell'archiviazione locale del thread (TLS) e tutto il codice in esecuzione in un determinato thread rileva tali dati. In un mondo asincrono, un'operazione logica può iniziare su un thread, sospendere e riprendere in un thread diverso. I dati locali del thread non seguono automaticamente—ExecutionContext li rende effettivamente seguire.
Come fluisce ExecutionContext
Acquisire ExecutionContext tramite ExecutionContext.Capture(). Ripristinarlo durante l'esecuzione di un delegato usando ExecutionContext.Run:
static void ExecutionContextCaptureDemo()
{
// Capture the current ExecutionContext
ExecutionContext? ec = ExecutionContext.Capture();
// Later, run a delegate within that captured context
if (ec is not null)
{
ExecutionContext.Run(ec, _ =>
{
// Code here sees the ambient state from the point of capture
Console.WriteLine("Running inside captured ExecutionContext.");
}, null);
}
}
Sub ExecutionContextCaptureExample()
' Capture the current ExecutionContext
Dim ec As ExecutionContext = ExecutionContext.Capture()
' Later, run a delegate within that captured context
If ec IsNot Nothing Then
ExecutionContext.Run(ec,
Sub(state)
' Code here sees the ambient state from the point of capture
Console.WriteLine("Running inside captured ExecutionContext.")
End Sub, Nothing)
End If
End Sub
Tutte le API asincrone in .NET che funzionano tramite fork, Run, QueueUserWorkItem, BeginRead e altre, acquisiscono ExecutionContext e usano il contesto archiviato quando si richiama il callback. Questo processo di acquisizione dello stato in un thread e ripristino su un altro è ciò che si intende per "flowing ExecutionContext".
Che cos'è SynchronizationContext?
SynchronizationContext è un'astrazione che rappresenta un ambiente di destinazione in cui si desidera eseguire il lavoro. I diversi framework dell'interfaccia utente offrono implementazioni specifiche:
- Windows Forms fornisce
WindowsFormsSynchronizationContext, che sovrascrive Post per chiamareControl.BeginInvoke. - macchine virtuali Windows fornisce
DispatcherSynchronizationContext, che esegue l'override di Post per chiamareDispatcher.BeginInvoke. - ASP.NET (in .NET Framework) ha fornito il proprio contesto che ha garantito la disponibilità di
HttpContext.Current.
SynchronizationContext Usando invece di API di marshalling specifiche del framework, è possibile scrivere componenti che funzionano tra framework dell'interfaccia utente:
static class SyncContextExample
{
public static void DoWork()
{
// Capture the current SynchronizationContext
SynchronizationContext? sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
// ... do work on the ThreadPool ...
if (sc is not null)
{
sc.Post(_ =>
{
// This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.");
}, null);
}
});
}
}
Class SyncContextExample
Public Shared Sub DoWork()
' Install a custom SynchronizationContext for demonstration
Dim customContext As New SimpleSynchronizationContext()
SynchronizationContext.SetSynchronizationContext(customContext)
' Capture the current SynchronizationContext
Dim sc As SynchronizationContext = SynchronizationContext.Current
ThreadPool.QueueUserWorkItem(
Sub(state)
' ... do work on the ThreadPool ...
If sc IsNot Nothing Then
sc.Post(
Sub(s)
' This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.")
End Sub, Nothing)
Else
Console.WriteLine("No SynchronizationContext was captured.")
End If
End Sub)
End Sub
End Class
' A minimal SynchronizationContext for demonstration purposes
Class SimpleSynchronizationContext
Inherits SynchronizationContext
Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
' Queue the callback to run on a thread pool thread
ThreadPool.QueueUserWorkItem(
Sub(s)
d(state)
End Sub)
End Sub
End Class
Acquisire un oggetto SynchronizationContext
Quando si acquisisce un SynchronizationContext, si legge il riferimento da SynchronizationContext.Current e lo si archivia per usarlo in un secondo momento. Si chiama Post sul riferimento acquisito per pianificare il lavoro in quell'ambiente.
Propagazione di ExecutionContext vs. uso di SynchronizationContext
Anche se entrambi i meccanismi comportano l'acquisizione dello stato da un thread, servono scopi diversi:
- Flowing ExecutionContext significa acquisire lo stato di ambiente e rendere lo stesso stato corrente durante l'esecuzione di un delegato. Il delegato viene eseguito ovunque venga eseguito e lo stato lo segue.
- L'uso di SynchronizationContext significa acquisire una destinazione di pianificazione e usarla per decidere dove viene eseguito un delegato. Il contesto acquisito controlla la posizione in cui viene eseguito il delegato.
In breve: ExecutionContext risponde "quale ambiente deve essere visibile?" mentre SynchronizationContext risponde "dove deve essere eseguito il codice?"
Come async/await interagisce con entrambi i contesti
L'infrastruttura async/await interagisce automaticamente con entrambi i contesti, ma in modi diversi.
ExecutionContext si propaga sempre
Ogni volta che un oggetto await sospende un metodo (perché il metodo awaiter restituisce IsCompleted), l'infrastruttura false acquisisce un oggetto ExecutionContext. Quando il metodo riprende, la continuazione viene eseguita all'interno del contesto acquisito. Questo comportamento è integrato nei generatori di metodi asincroni (ad esempio , AsyncTaskMethodBuilder) e si applica indipendentemente dal tipo di awaitable in uso.
SuppressFlow() esiste, ma non è un interruttore specifico per await come ConfigureAwait(false). Sopprime l'acquisizione ExecutionContext per il lavoro che si accoda mentre la soppressione è attiva. Non fornisce un'opzione perawait ogni modello di programmazione che indica ai generatori di metodi asincroni di ignorare il ripristino dell'oggetto acquisito ExecutionContext per una continuazione. Questa progettazione è intenzionale perché ExecutionContext è il supporto a livello di infrastruttura che simula la semantica locale dei thread in un mondo asincrono e la maggior parte degli sviluppatori non deve mai pensarci.
Gli task awaiter catturano il SynchronizationContext
Gli awaiters per Task e Task<TResult> includono il supporto per SynchronizationContext. I generatori di metodi asincroni non includono questo supporto.
Quando si esegue await un'attività:
- Il awaiter controlla SynchronizationContext.Current.
- Se esiste un contesto, l’awaiter lo acquisisce.
- Al termine dell'attività, la continuazione viene ripristinata in tale contesto salvato anziché essere eseguita nel thread di completamento o nel pool di thread.
Questo comportamento è come await "ti riporta dove eri". Ad esempio, continuare il thread dell'interfaccia utente in un'applicazione desktop.
ConfigureAwait controlla l'acquisizione del SynchronizationContext
Se non desideri il comportamento di marshalling, chiama ConfigureAwait con false:
await task.ConfigureAwait(false);
Quando si imposta continueOnCapturedContext su false, l'awaiter non verifica la presenza di un SynchronizationContext e la continuazione viene eseguita ovunque l'attività venga completata (in genere in un thread del pool di thread). Gli autori di librerie devono usare ConfigureAwait(false) in ogni await, a meno che il codice non debba riprendere in modo specifico nel contesto acquisito.
SynchronizationContext.Current non viene propagato attraverso 'awaits'
Questo punto è il più importante: SynchronizationContext.Currentnon prosegue attraverso i punti await. I generatori di metodi asincroni nel runtime usano overload interni che sopprimono esplicitamente SynchronizationContext dal fluire come parte di ExecutionContext.
Perché è importante:
Tecnicamente, SynchronizationContext è uno dei contesti secondari che ExecutionContext possono contenere. Se è stato eseguito come parte di ExecutionContext, il codice in esecuzione in un thread del pool di thread potrebbe visualizzare un'interfaccia utente SynchronizationContext come Current, non perché tale thread è il thread dell'interfaccia utente, ma perché il contesto è stato "traspelato" tramite flusso. Tale modifica altererebbe il significato di SynchronizationContext.Current da "ambiente in cui sono attualmente in" a "l'ambiente che storicamente esisteva da qualche parte nella catena di chiamate".
Esempio di Task.Run
Si consideri il codice che delega il lavoro al pool di thread. Il comportamento del thread dell'interfaccia utente descritto di seguito si applica solo quando SynchronizationContext.Current non è Null, ad esempio in un'app dell'interfaccia utente:
static class TaskRunExample
{
public static async Task ProcessOnUIThread()
{
// This method is called from a thread with a SynchronizationContext.
// Task.Run offloads work to the thread pool.
string result = await Task.Run(async () =>
{
string data = await DownloadAsync();
// Compute runs on the thread pool, not the original context,
// because SynchronizationContext doesn't flow into Task.Run.
return Compute(data);
});
// Back on the original context (the continuation is posted back).
Console.WriteLine(result);
}
private static async Task<string> DownloadAsync()
{
await Task.Delay(100);
return "downloaded data";
}
private static string Compute(string data) =>
$"Computed: {data.Length} chars";
}
Class TaskRunExampleClass
Public Shared Async Function ProcessOnUIThread() As Task
' If a SynchronizationContext is present when this method starts,
' the outer await captures it. Task.Run still offloads work to the thread pool.
Dim result As String = Await Task.Run(
Async Function()
Dim data As String = Await DownloadAsync()
' Compute runs on the thread pool, not the caller's context,
' because SynchronizationContext doesn't flow into Task.Run.
Return Compute(data)
End Function)
' Resume on the captured context, if one was available.
Console.WriteLine(result)
End Function
Private Shared Async Function DownloadAsync() As Task(Of String)
Await Task.Delay(100)
Return "downloaded data"
End Function
Private Shared Function Compute(data As String) As String
Return $"Computed: {data.Length} chars"
End Function
End Class
In un'applicazione console, SynchronizationContext.Current è in genere null, quindi il codice non riprende in un thread reale dell'interfaccia utente. Il frammento di codice illustra invece la regola concettualmente: se un'interfaccia utente SynchronizationContext è stata propagata attraverso await punti, la parte interna await del delegato passato a Task.Run vedrebbe quel contesto dell'interfaccia utente come Current. La continuazione dopo await DownloadAsync() avrebbe quindi fatto sì che si effettuasse un postback al thread dell'interfaccia utente, causando Compute(data) ad essere eseguito nel thread dell'interfaccia utente anziché nel pool di thread. Questo comportamento sconfigge lo scopo della Task.Run chiamata.
Poiché il runtime elimina il flusso in ExecutionContext, await all'interno di Task.Run non eredita un contesto UI esterno e la continuazione rimane eseguita nel pool di thread come da previsione.
Sommario
| Aspetto | ExecutionContext | SynchronizationContext |
|---|---|---|
| Purpose | Trasporta lo stato ambientale attraverso i limiti asincroni | Rappresenta un pianificatore di destinazione (dove deve essere eseguito il codice) |
| Acquisito da | Generatori di metodi asincroni (infrastruttura) | Gestori di attesa delle attività (await task) |
| Flussi in attesa di completamento? | Sì, sempre | No: catturati e pubblicati su, non incanalati |
| API di eliminazione |
ExecutionContext.SuppressFlow (avanzato; raramente necessario) |
ConfigureAwait(false) |
| Scope | Tutti gli oggetti awaitable |
Task e Task<TResult> (i awaiter personalizzati possono aggiungere logica simile) |