Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Lorsque vous travaillez avec async et awaitque deux types de contexte jouent des rôles importants mais très différents : ExecutionContext et SynchronizationContext. Vous apprenez ce que chacun d’eux fait, comment chacun interagit avec async/await, et pourquoi SynchronizationContext.Current ne se propage pas à travers les points d'attente.
Qu’est-ce que ExecutionContext ?
ExecutionContext est un conteneur pour l’état ambiant qui circule avec le flux de contrôle logique de votre programme. Dans un monde synchrone, les informations ambiantes résident dans le stockage local de threads (TLS) et tout le code s’exécutant sur un thread donné voit ces données. Dans un monde asynchrone, une opération logique peut démarrer sur un thread, suspendre et reprendre sur un autre thread. Les données locales de thread ne suivent pas automatiquement : ExecutionContext les fait suivre.
Flux de ExecutionContext
Capturer ExecutionContext à l'aide de ExecutionContext.Capture(). Restaurez-le lors de l’exécution d’un délégué en utilisant 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
Toutes les API asynchrones de .NET qui forment une bifurcation—Run, QueueUserWorkItem, BeginRead, etc.—capturent ExecutionContext et utilisent le contexte stocké lors de l'invocation de votre rappel. Ce processus de capture d’état sur un thread et de le restaurer sur un autre est ce que signifie "flowing ExecutionContext".
Qu’est-ce que SynchronizationContext ?
SynchronizationContext est une abstraction qui représente un environnement cible dans lequel vous souhaitez exécuter le travail. Les différentes infrastructures d’interface utilisateur fournissent leurs propres implémentations :
- Windows Forms fournit
WindowsFormsSynchronizationContext, qui remplace Post pour appelerControl.BeginInvoke. - WPF fournit
DispatcherSynchronizationContext, qui remplace Post pour appelerDispatcher.BeginInvoke. - ASP.NET (sur .NET Framework) a fourni son propre contexte qui a assuré que
HttpContext.Currentétait disponible.
En utilisant SynchronizationContext au lieu des API de marshaling spécifiques aux infrastructures, vous pouvez écrire des composants qui fonctionnent dans les frameworks d’interface utilisateur :
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
Capturer un SynchronizationContext
Lorsque vous capturez un SynchronizationContext, vous lisez la référence à partir de SynchronizationContext.Current et la stockez pour une utilisation ultérieure. Vous appelez ensuite Post sur la référence capturée pour planifier le travail retour dans cet environnement.
Propagation d'ExecutionContext par rapport à l'utilisation de SynchronizationContext
Bien que les deux mécanismes impliquent la capture d’état à partir d’un thread, ils servent différents objectifs :
- Flowing ExecutionContext signifie capturer l’état ambiant et faire en sorte que cet état soit actuel pendant l’exécution d’un délégué. Le délégué s'exécute là où il finit par s'exécuter, et l'état le suit.
- L’utilisation de SynchronizationContext signifie capturer une cible de planification et l’utiliser pour décider où un délégué s’exécute. Le contexte capturé contrôle l’exécution du délégué.
En bref : ExecutionContext répond à « quel environnement doit être visible ? », tandis que SynchronizationContext dit « où le code doit-il s’exécuter ? »
Comment async/await interagit avec les deux contextes
L’infrastructure async/await interagit automatiquement avec les deux contextes, mais de différentes manières.
ExecutionContext s'écoule toujours
Chaque fois qu'une méthode await est suspendue (car l'attenteur IsCompleted retourne false), l'infrastructure capture un ExecutionContext. Lorsque la méthode reprend son exécution, la continuation s’exécute dans le contexte capturé. Ce comportement est intégré aux générateurs de méthodes asynchrones (par exemple, AsyncTaskMethodBuilder) et s’applique quel que soit l'awaitable que vous utilisez.
SuppressFlow() existe, mais ce n’est pas un commutateur spécifique à await comme ConfigureAwait(false). Il supprime la ExecutionContext capture pour le travail que vous ajoutez à la file d'attente lorsque la suppression est active. Il ne fournit pas d’option pourawait le modèle de programmation qui indique aux générateurs de méthodes asynchrones d’ignorer la capture de ExecutionContext lors de la restauration pour une continuation. Cette conception est intentionnelle car ExecutionContext représente une prise en charge au niveau de l’infrastructure qui simule la sémantique de thread-local dans un monde asynchrone, et la plupart des développeurs n’ont jamais besoin d’y penser.
Les gestionnaires d'attente des tâches capturent SynchronizationContext
Les awaiters pour Task et Task<TResult> incluent la prise en charge pour SynchronizationContext. Les générateurs de méthodes asynchrones n’incluent pas cette prise en charge.
Quand vous await effectuez une tâche :
- L'attendant vérifie SynchronizationContext.Current.
- S’il existe un contexte, l’awaiter le capture.
- Une fois la tâche terminée, la continuation est renvoyée à ce contexte capturé, au lieu d'être exécutée sur le thread qui a complété la tâche ou sur le pool de threads.
C'est ainsi que await « vous ramène là où vous étiez ». Par exemple, reprendre sur le fil d'exécution d'interface utilisateur dans une application de bureau.
ConfigureAwait contrôle la capture du `SynchronizationContext`
Si vous ne souhaitez pas le comportement de marshaling, appelez ConfigureAwait avec false:
await task.ConfigureAwait(false);
Lorsque vous définissez continueOnCapturedContext à false, l'awaiter ne vérifie pas SynchronizationContext et la continuation s'exécute là où la tâche se termine (généralement sur un thread du pool de threads). Les auteurs de bibliothèque doivent utiliser ConfigureAwait(false) sur chaque await, sauf si le code doit spécifiquement reprendre sur le contexte capturé.
SynchronizationContext.Current ne se propage pas à travers les awaits
Ce point est le plus important : SynchronizationContext.Currentne circule pas entre les points d’attente . Les générateurs de méthodes asynchrones de l'environnement d'exécution utilisent des surcharges internes qui empêchent explicitement SynchronizationContext de faire partie de ExecutionContext.
Pourquoi est-ce important ?
Techniquement, SynchronizationContext est l'un des sous-contextes que ExecutionContext peut contenir. Si cela a été transmis dans le cadre de ExecutionContext, le code s'exécutant sur un thread de pool de threads peut percevoir une interface utilisateur SynchronizationContext comme Current, ceci n’est pas dû au fait que ce thread soit le thread d'interface utilisateur, mais parce que le contexte "s'écoule" de manière inappropriée via le flux. Ce changement modifierait la signification de SynchronizationContext.Current « l’environnement dans lequel je suis actuellement » à « l’environnement qui existait historiquement quelque part dans la chaîne d’appels ».
L'exemple Task.Run
Considérez le code qui alloue des tâches dans le pool de threads. Le comportement du thread d’interface utilisateur décrit ici s’applique uniquement lorsqu’il SynchronizationContext.Current n’est pas null, par exemple dans une application d’interface utilisateur :
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
Dans une application console, SynchronizationContext.Current est généralement null, donc l'extrait de code ne s'exécute pas sur un thread d'interface utilisateur réel. Au lieu de cela, l’extrait de code illustre la règle conceptuellement : si une interface utilisateur SynchronizationContext s'est écoulée à travers plusieurs await points, le await à l'intérieur du délégué passé à Task.Run verrait ce contexte d'interface utilisateur comme Current. La continuation après await DownloadAsync() retournerait alors au thread d'interface utilisateur, ce qui provoque l’exécution de Compute(data) sur le thread d'interface utilisateur plutôt que sur le pool de threads. Ce comportement élimine l’objectif de l’appel Task.Run .
Étant donné que le runtime supprime SynchronizationContext le flux dans ExecutionContext, le await à l’intérieur de Task.Run n’hérite pas d’un contexte d’interface utilisateur externe et la continuation continue à s’exécuter sur le pool de threads comme il se doit.
Résumé
| Aspect | Contexte d'exécution | SynchronizationContext |
|---|---|---|
| Purpose | Transporte l’état ambiant au-delà des limites asynchrones | Représente un planificateur cible (où le code doit s’exécuter) |
| Capturé par | Générateurs de méthodes asynchrones (infrastructure) | Awaiters de tâche (await task) |
| Flux à travers await ? | Oui, toujours | Non : capturé et publié sur, non diffusé |
| API de suppression |
ExecutionContext.SuppressFlow (avancé ; rarement nécessaire) |
ConfigureAwait(false) |
| Étendue | Tous les attendus |
Task et Task<TResult> (les awaiters personnalisés peuvent ajouter une logique similaire) |