Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Wenn Sie mit async und await arbeiten, spielen zwei Kontexttypen wichtige, aber sehr unterschiedliche Rollen: ExecutionContext und SynchronizationContext. Sie lernen, wie jeder funktioniert, wie jeder mit async/await interagiert, und warum SynchronizationContext.Current nicht über die Await-Punkte fließt.
Was ist ExecutionContext?
ExecutionContext ist ein Container für den Umgebungszustand, der mit dem logischen Kontrollfluss Ihres Programms fließt. In einer synchronen Welt leben Umgebungsinformationen im threadlokalen Speicher (TLS), und der gesamte Code, der in einem bestimmten Thread ausgeführt wird, sieht diese Daten. In einer asynchronen Welt kann ein logischer Vorgang in einem Thread gestartet, angehalten und auf einem anderen Thread fortgesetzt werden. Thread-lokale Daten folgen nicht automatisch - ExecutionContext sorgt dafür, dass sie folgen.
So fließt ExecutionContext
Erfassen ExecutionContext mithilfe von ExecutionContext.Capture(). Stellen Sie sie während der Ausführung eines Delegaten wieder her, indem Sie ExecutionContext.Run verwenden:
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
Alle asynchronen APIs in .NET, die einen Fork ausführen – Run, QueueUserWorkItem, BeginRead und andere – erfassen ExecutionContext und verwenden den gespeicherten Kontext, wenn Sie Ihren Callback aufrufen. Dieser Prozess der Erfassung des Zustands in einem Thread und das Wiederherstellen des Zustands auf einem anderen ist das, was "flowing ExecutionContext" bedeutet.
Was ist SynchronizationContext?
SynchronizationContext ist eine Abstraktion, die eine Zielumgebung darstellt, in der Sie arbeiten möchten. Verschiedene Benutzeroberflächenframeworks bieten eigene Implementierungen:
- Windows Forms stellt
WindowsFormsSynchronizationContextbereit, die Post außer Kraft setzt, umControl.BeginInvokeaufzurufen. - WPF stellt
DispatcherSynchronizationContextbereit, die Post außer Kraft setzt, umDispatcher.BeginInvokeaufzurufen. - ASP.NET (im .NET Framework) stellte einen eigenen Kontext bereit, der sicherstellte, dass
HttpContext.Currentverfügbar war.
Anstatt framework-spezifische Marshaling-APIs zu verwenden, können Sie mit SynchronizationContext Komponenten schreiben, die über Benutzeroberflächen-Frameworks hinweg funktionieren.
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
Ressourcensynchronisierung
Wenn Sie ein SynchronizationContext erfassen, lesen Sie die Referenz aus SynchronizationContext.Current und speichern sie zur späteren Verwendung. Dann rufen Sie Post mit der erfassten Referenz auf, um die Arbeit in dieser Umgebung zu planen.
Flowing ExecutionContext vs. Verwendung des SynchronizationContext
Obwohl beide Mechanismen das Erfassen des Zustands aus einem Thread umfassen, dienen sie unterschiedlichen Zwecken:
- Flowing ExecutionContext bedeutet, dass der Umgebungszustand erfasst und derselbe Zustand während der Ausführung eines Delegaten wiederhergestellt wird. Der Delegat wird dort ausgeführt, wo er ausgeführt wird – der Zustand folgt ihm.
- Die Verwendung von SynchronizationContext bedeutet, dass ein Schedulingziel erfasst und verwendet wird, um zu bestimmen, wo ein Delegat ausgeführt wird. Der erfasste Kontext steuert, wo der Delegat ausgeführt wird.
Kurz gesagt: ExecutionContext Antwort : "Welche Umgebung sollte sichtbar sein?" und SynchronizationContext antwortt auf "Wo sollte der Code ausgeführt werden?"
Wie async/await mit verschiedenen Kontexten interagiert
Die async/await Infrastruktur interagiert automatisch mit beiden Kontexten, aber auf unterschiedliche Weise.
ExecutionContext wird immer weitergegeben
Immer wenn ein await eine Methode unterbricht (weil der IsCompleted des Awaiters false zurückgibt), erfasst die Infrastruktur einen ExecutionContext. Wenn die Methode fortgesetzt wird, wird die Fortsetzung im erfassten Kontext ausgeführt. Dieses Verhalten ist in die asynchronen Methoden-Builder (z. B. AsyncTaskMethodBuilder) integriert und gilt unabhängig davon, welche Art von Awaitable Sie verwenden.
SuppressFlow() existiert, aber es ist kein für await spezifischer Schalter wie ConfigureAwait(false). Unterdrückt die Erfassung von ExecutionContext für Arbeit, die Sie während der aktiven Unterdrückung in die Warteschlange einreihen. Es stellt keine per-awaitProgramming-Model-Option bereit, die den Async-Methoden-Buildern mitteilt, die Wiederherstellung des erfassten ExecutionContext für eine Fortsetzung zu überspringen. Dieses Design ist beabsichtigt, da es sich um ExecutionContext Unterstützung auf Infrastrukturebene handelt, die threadlokale Semantik in einer asynchronen Welt simuliert und die meisten Entwickler nie darüber nachdenken müssen.
Task awaiters capture SynchronizationContext
Die Awaiter für Task und Task<TResult> enthalten Unterstützung für SynchronizationContext. Die asynchronen Methoden-Generatoren enthalten diese Unterstützung nicht.
Wenn Sie await eine Aufgabe ausführen:
- Der Awaiter überprüft SynchronizationContext.Current.
- Wenn ein Kontext vorhanden ist, erfasst der Awaiter ihn.
- Wenn die Aufgabe abgeschlossen ist, wird die Fortsetzung wieder in den erfassten Kontext zurückgesendet, anstatt auf dem abschließenden Thread oder im Threadpool ausgeführt zu werden.
Mit diesem Verhalten bringt await Sie "dorthin zurück, wo Sie waren". Zum Beispiel die Fortsetzung auf dem UI-Thread in einer Desktop-Anwendung.
ConfigureAwait steuert die SynchronizationContext-Erfassung
Wenn Sie das Marshaling-Verhalten nicht wünschen, rufen Sie ConfigureAwait mit false auf:
await task.ConfigureAwait(false);
Wenn Sie continueOnCapturedContext auf false setzen, prüft der Awaiter nicht, ob ein SynchronizationContext vorhanden ist, und die Fortsetzung wird dort ausgeführt, wo die Aufgabe abgeschlossen wird (normalerweise auf einem Threadpool-Thread). Bibliotheksautoren sollten ConfigureAwait(false) bei jedem Await verwenden, es sei denn, der Code muss explizit auf dem erfassten Kontext fortgesetzt werden.
SynchronizationContext.Current wird nicht über Awaits hinweg weitergegeben
Dieser Punkt ist der wichtigste: SynchronizationContext.Currentwird nicht wird nicht über Await-Punkte hinweg weitergegeben. Die asynchronen Methoden-Ersteller im Laufzeitsystem verwenden interne Überladungen, die explizit SynchronizationContext daran hindern, als Teil von ExecutionContext zu fließen.
Warum das wichtig ist
Technisch gesehen ist SynchronizationContext einer der Unterkontexte, die ExecutionContext enthalten kann. Wenn der Code als Teil von ExecutionContext übertragen wurde, könnte Code, der in einem Thread im Threadpool ausgeführt wird, eine Benutzeroberfläche SynchronizationContext als Current sehen, nicht weil dieser Thread der UI-Thread ist, sondern weil der Kontext durch die Übertragung "durchsickerte". Diese Änderung würde die Bedeutung von SynchronizationContext.Current "der Umgebung, in der ich gerade bin" in "die Umgebung ändern, die historisch irgendwo in der Anrufkette existierte.".
Beispiel "Task.Run"
Erwägen Sie Code, der die Arbeit in den Thread-Pool auslagert. Das hier beschriebene Verhalten des UI-Threads gilt nur, wenn SynchronizationContext.Current nicht null ist, etwa in einer UI-App:
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 einer Konsolenanwendung ist SynchronizationContext.Current in der Regel null, so dass das Snippet nicht auf einem echten UI-Thread fortgesetzt wird. Stattdessen veranschaulicht der Codeausschnitt das Prinzip konzeptionell: Wenn ein UI-SynchronizationContext über await hinweg weitergegeben würde, würde der await innerhalb des an Task.Run übergebenen Delegaten diesen UI-Kontext als Current sehen. Die Fortsetzung danach await DownloadAsync() würde dann wieder in den UI-Thread zurückgesendet, sodass Compute(data) im UI-Thread statt im Threadpool ausgeführt wird. Dieses Verhalten verfehlt den Zweck des Task.Run Aufrufs.
Da die Laufzeit die SynchronizationContext-Weitergabe in ExecutionContext unterdrückt, erbt die await innerhalb von Task.Run keinen äußeren UI-Kontext, und die Fortsetzung läuft wie vorgesehen weiterhin auf dem Threadpool.
Zusammenfassung
| Aspekt | Ausführungskontext | SynchronizationContext |
|---|---|---|
| Purpose | Trägt den Umgebungszustand über asynchrone Grenzen hinweg | Stellt einen Ziel-Scheduler dar (in dem Code laufen soll) |
| Erfasst von | Asynchrone Methoden-Ersteller (Infrastruktur) | Task-Awaiter (await task) |
| Wird über Await hinweg weitergegeben? | Ja, immer | Nein – wird erfasst und dorthin gepostet, nicht weitergegeben. |
| Supprimierungs-API |
ExecutionContext.SuppressFlow (erweitert; selten erforderlich) |
ConfigureAwait(false) |
| Bereich | Alle Awaitables |
Task und Task<TResult> (benutzerdefinierte Awaiters können eine ähnliche Logik hinzufügen) |