Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Při práci s async a await hrají dva typy kontextu důležité, ale velmi odlišné role: ExecutionContext a SynchronizationContext. Dozvíte se, co každý z nich dělá, jak každý komunikuje async/awaita proč SynchronizationContext.Current neprotéká mezi body await.
Co je ExecutionContext?
ExecutionContext je kontejner pro stav okolí, který plyne s logickým tokem řízení vašeho programu. V synchronním světě se informace o okolí nacházejí v místním úložišti s vlákny (TLS) a veškerý kód spuštěný v daném vlákně vidí tato data. V asynchronním světě může logická operace začínat na jednom vlákně, pozastavit a pokračovat v jiném vlákně. Data místního vlákna nenásledují automaticky – ExecutionContext způsobuje, že se řídí.
Jak toky ExecutionContext
Zachyťte ExecutionContext pomocí ExecutionContext.Capture(). Obnovte během spuštění delegáta pomocí 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
Všechna asynchronní rozhraní API v .NET, která rozdělují práci – Run, QueueUserWorkItem, BeginRead a další – zachycují ExecutionContext a při vyvolání zpětného volání používají uložený kontext. Tento proces zachycení stavu v jednom vlákně a jeho obnovení na jiném je to, co znamená "flowing ExecutionContext".
Co je SynchronizationContext?
SynchronizationContext je abstrakce, která představuje cílové prostředí, ve kterém chcete pracovat. Různé architektury uživatelského rozhraní poskytují vlastní implementace:
- model Windows Forms poskytuje
WindowsFormsSynchronizationContext, který přepisuje Post pro voláníControl.BeginInvoke. - WPF (Windows Presentation Foundation) poskytuje
DispatcherSynchronizationContext, který přepisuje Post a voláDispatcher.BeginInvoke. - ASP.NET (v rozhraní .NET Framework) poskytl vlastní kontext, který zajistil, že byl k dispozici
HttpContext.Current.
SynchronizationContext Místo framework-specifických API pro zařazování můžete psát komponenty, které fungují v různých rámcích uživatelského rozhraní:
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
Zachycení synchronizačníhocontextu
Když SynchronizationContext zachytíte, přečtete si odkaz z SynchronizationContext.Current a uložíte ho pro pozdější použití. Potom zavoláte Post na zachyceném odkazu, a naplánujete práci zpět do daného prostředí.
Tok ExecutionContextu vs. použití SynchronizationContextu
I když oba mechanismy zahrnují zachycení stavu z vlákna, slouží různým účelům:
- Propagace ExecutionContext znamená zachycení okolního stavu a zachování tohoto stavu aktuálním během provádění delegáta. Delegát běží tam, kam se dostane – stav ho následuje.
- Použití SynchronizationContext znamená zaznamenání plánu cíle a jeho použití k rozhodnutí, kde se delegát provádí. Zachycený kontext řídí, kde delegát běží.
Stručně řečeno: ExecutionContext odpovídá na otázku "jaké prostředí by mělo být viditelné?" zatímco SynchronizationContext odpovídá na otázku "kde by měl kód běžet?"
Jak async/await komunikuje s oběma kontexty
Infrastruktura async/await komunikuje s oběma kontexty automaticky, ale různými způsoby.
ExecutionContext vždy se propaguje
Kdykoli await pozastaví metodu (protože awaiter IsCompleted vrátí false), infrastruktura zachytí ExecutionContext. Když se metoda obnoví, pokračování se spustí v zachyceném kontextu. Toto chování je integrované do tvůrců asynchronních metod (například AsyncTaskMethodBuilder) a platí bez ohledu na to, jaký typ awaitable objektu použijete.
SuppressFlow() existuje, ale nejedná se o přepínač specifický pro await, jako je ConfigureAwait(false). Potlačí zachytávání ExecutionContext pro práci, kterou zařadíte do fronty, zatímco potlačení je aktivní. Neposkytuje možnost podle programovacíhoawait modelu, která dává tvůrcům asynchronních metod pokyn, aby přeskočí obnovení zachyceného ExecutionContext pro pokračování. Tento návrh je záměrný, protože ExecutionContext se jedná o podporu na úrovni infrastruktury, která simuluje sémantiku místních vláken v asynchronním světě a většina vývojářů o tom nemusí přemýšlet.
Funkce Awaiters zachytává SynchronizačníContext
Uživatelé čekající na Task a Task<TResult> zahrnují podporu pro SynchronizationContext. Tvůrci asynchronních metod tuto podporu nezahrnují.
Když await úkol:
- Awaiter zkontroluje SynchronizationContext.Current.
- Pokud kontext existuje, operátor await ho zachytí.
- Po dokončení úlohy se pokračování publikuje zpět do tohoto zachyceného kontextu místo spuštění v dokončeném vlákně nebo fondu vláken.
Toto chování je způsob, jak await vás "vrátí do místa, kde jste byli". Například obnovení vlákna uživatelského rozhraní v desktopové aplikaci.
Konfigurace ConfigureAwait určuje zachycení SynchronizationContext
Pokud nechcete způsob zařazení, zavolejte funkci s ConfigureAwaitfalse:
await task.ConfigureAwait(false);
Když nastavíte continueOnCapturedContext na false, awaiter nekontroluje SynchronizationContext a pokračování pokračuje na libovolném místě, kde se úloha dokončí (obvykle ve vlákně fondu vláken). Autoři knihoven by měli použít ConfigureAwait(false) pro každý await, pokud kód výslovně nepotřebuje obnovit v zachyceném kontextu.
SynchronizationContext.Current se nepřenáší přes operátory await
Tento bod je nejdůležitější: SynchronizationContext.Currentnepřechází přes body await. Tvůrci asynchronních metod v modulu runtime využívají interní přetížení, která explicitně zabraňují SynchronizationContext v přechodu jako součást ExecutionContext.
Proč na tom záleží
Technicky vzato SynchronizationContext je jedním z dílčích kontextů, které ExecutionContext můžou obsahovat. Pokud teklo jako součást ExecutionContext, kód spuštěný na vlákně fondu vláken může vidět uživatelské rozhraní jako SynchronizationContext, ne Current proto, že toto vlákno je vlákno uživatelského rozhraní, ale protože kontext "unikl" prostřednictvím přenosu toku. Tato změna by změnila význam SynchronizationContext.Current z "prostředí, ve které právě jsem" na "prostředí, které v minulosti existovalo někde v řetězci volání".
Příklad Task.Run
Vezměte v úvahu kód, který přesměruje práci do fondu vláken. Chování vlákna uživatelského rozhraní popsané zde platí pouze v případě, že SynchronizationContext.Current není null, například v aplikaci uživatelského rozhraní:
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
V konzolové aplikaci SynchronizationContext.Current je obvykle null, takže fragment kódu nebude pokračovat ve skutečném vlákně uživatelského rozhraní. Místo toho fragment kódu znázorňuje pravidlo koncepčně: pokud uživatelské rozhraní SynchronizationContext proudí přes await body, await, který byl předán delegátovi Task.Run, mohl vnímat kontext uživatelského rozhraní jako Current. Pokračování poté, co await DownloadAsync() odešle zpět do vlákna uživatelského rozhraní, způsobí, že se Compute(data) spustí ve vlákně uživatelského rozhraní, místo ve fondu vláken. Toto chování porazí účel Task.Run volání.
Vzhledem k tomu, že modul runtime potlačuje SynchronizationContext tok v ExecutionContext, await uvnitř Task.Run nedědí vnější kontext uživatelského rozhraní a pokračování běží ve fondu vláken podle plánu.
Shrnutí
| Aspekt | Kontext provádění | SynchronizationContext |
|---|---|---|
| Purpose | Přenáší okolní stav přes asynchronní hranice. | Představuje cílový plánovač (kde by se měl spustit kód). |
| Zachyceno | Asynchronní metoda budování (infrastruktura) | Awaiterů úkolů (await task) |
| Průtoky napříč await? | Ano, vždy | Ne – zachyceno a zveřejněno, nikoliv přeposláno |
| API pro potlačení |
ExecutionContext.SuppressFlow (pokročilé, zřídka potřebné) |
ConfigureAwait(false) |
| Scope | Všechny čekající úlohy |
Task a Task<TResult> (vlastní čekací objekty mohou přidat podobnou logiku) |