ConfigureAwait : Le guide essentiel
C’est quoi ConfigureAwait ?
Quand tu utilises await sur une tâche, par défaut, ton code reprend son exécution sur le même contexte (thread UI, contexte de synchronisation, etc.). ConfigureAwait te permet de contrôler ce comportement.
Exemple simple :
// (async void = uniquement pour les handlers, jamais dans les libs)
private async void Button_Click(object sender, EventArgs e)
{
// On est sur le thread UI
var data = await DownloadDataAsync();
// await a automatiquement capturé le contexte UI
// Donc on est TOUJOURS sur le thread UI
// On peut modifier l'interface sans problème
textBox.Text = data; // ✅ Fonctionne !
}
private async void Button_ClickWithoutContext(object sender, EventArgs e)
{
// On est sur le thread UI
var data = await DownloadDataAsync().ConfigureAwait(false);
// Ici, le contexte de synchronisation UI n’a PAS été capturé
// Le code reprend sur un thread du pool (pas le thread UI)
textBox.Text = data; // ❌ Exception : InvalidOperationException
// "Le contrôle appartient à un autre thread."
}
Sans ConfigureAwait, await est “gentil” : il te ramène toujours là où tu étais. Avec ConfigureAwait(false), tu dis “je m’en fiche où je reprends”. Pratique pour les bibliothèques, dangereux pour l’UI.
ConfigureAwait est souvent mal compris. Pourtant, une mauvaise utilisation peut te coûter des performances, voire provoquer des erreurs de thread (InvalidOperationException).
Comportement par défaut (sans ConfigureAwait)
sequenceDiagram
participant UI as Thread UI
participant Task as Tâche Async
participant Pool as Thread Pool
UI->>Task: await SomethingAsync()
Note over UI: Capture le contexte UI
Task->>Pool: Exécution asynchrone
Pool-->>Task: Tâche terminée
Task->>UI: Reprend sur le thread UI
Note over UI: Peut mettre à jour l'interface
Avec ConfigureAwait(false)
sequenceDiagram
participant UI as Thread UI
participant Task as Tâche Async
participant Pool as Thread Pool
UI->>Task: await SomethingAsync()<br/>.ConfigureAwait(false)
Note over UI: Ne capture PAS le contexte
Task->>Pool: Exécution asynchrone
Pool-->>Task: Tâche terminée
Task->>Pool: Reprend sur un thread pool
Note over Pool: ⚠️ Ne peut PAS mettre<br/>à jour l'interface
La règle d’or
ConfigureAwait(false) dans le code de bibliothèque, pas dans le code de l’application.
Quand utiliser ConfigureAwait(false) ?
✅ Dans les bibliothèques utilitaires
- Tu écris du code qui ne touche jamais à l’interface graphique
- Tu veux optimiser les performances
- Exemple : librairies de traitement de données, clients HTTP, accès base de données
// Dans une bibliothèque
public async Task<string> GetDataAsync()
{
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return content;
}
❌ Ne pas utiliser dans le code d’application
- Code UI (WPF, WinForms, MAUI)
- Contrôleurs ASP.NET Core (ce n’est plus nécessaire depuis Core 2.0)
- Partout où tu dois revenir sur le thread principal
// Dans une application UI - PAS de ConfigureAwait(false)
private async void Button_Click(object sender, EventArgs e)
{
var data = await GetDataAsync(); // Pas de ConfigureAwait ici !
textBox.Text = data; // On doit être sur le thread UI
}
Idées reçues à éviter
❌ ConfigureAwait(false) n’évite pas les deadlocks - Ce n’est pas son rôle. La vraie solution : ne jamais bloquer du code async avec .Result ou .Wait()
❌ ConfigureAwait configure l’await, pas la Task - Il doit être juste après le await
// ❌ Incorrect - ne fait rien
var task = SomethingAsync();
task.ConfigureAwait(false);
await task;
// ✅ Correct
await SomethingAsync().ConfigureAwait(false);
❌ Une fois configuré, il s’applique partout - Non, chaque await doit le préciser explicitement.
Nouveautés .NET 8 : ConfigureAwaitOptions
.NET 8 introduit plus de flexibilité avec ConfigureAwaitOptions :
// Équivalent à ConfigureAwait(false)
await task.ConfigureAwait(ConfigureAwaitOptions.None);
// Ignorer les exceptions (utile pour l'annulation)
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// Forcer un comportement asynchrone (même si la tâche est déjà terminée)
await task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
// Combiner plusieurs options
await task.ConfigureAwait(
ConfigureAwaitOptions.None | ConfigureAwaitOptions.SuppressThrowing
);
Option SuppressThrowing
Pratique quand tu veux attendre qu’une tâche se termine sans te soucier des exceptions :
// Annuler et attendre la fin de la tâche
_cts.Cancel();
await _task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// Démarrer la nouvelle tâche
_task = NewTaskAsync(_cts.Token);
⚠️ Attention : Ne fonctionne qu’avec Task, pas avec Task<T> (sinon exception au runtime).
Option ForceYielding
Force l’await à se comporter de manière asynchrone, même si la tâche est déjà terminée :
// Utile en tests ou pour éviter les stack dives
await task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
sequenceDiagram
%% --- ForceYielding ---
rect rgb(230, 255, 230)
note over Caller,Task: Avec ConfigureAwaitOptions.ForceYielding
participant Caller as Code appelant
participant Awaiter as Awaiter
participant Scheduler as SynchronizationContext/ThreadPool
participant Task as Task
Caller->>Task: await task.ConfigureAwait(ForceYielding)
Note right of Task: La tâche est déjà terminée
Task-->>Awaiter: Retourne l'état terminé
Awaiter->>Scheduler: Force une reprise asynchrone (Post/Queue)
Scheduler-->>Caller: Planifie la continuation sur un autre cycle
Caller->>Caller: Exécute la suite du code (Continuation)
Note over Caller: L'await se comporte de manière<br/>asynchrone malgré la complétion immédiate
end
%% --- Par défaut ---
rect rgb(255, 240, 240)
note over Caller,Task: Comportement par défaut (sans ForceYielding)
Caller->>Task: await task
Note right of Task: La tâche est déjà terminée
Task-->>Awaiter: Retourne l'état terminé
Awaiter-->>Caller: Exécute immédiatement la continuation<br/>(même pile)
Caller->>Caller: Continue le code sans repasser par le scheduler
Note over Caller: Exécution synchrone<br/>-> risque de stack dive
end
En résumé
- Bibliothèque -> Utilise
ConfigureAwait(false)partout - Application -> N’utilise pas
ConfigureAwait(false) - ASP.NET Core > 2.0 -> Pas besoin de
ConfigureAwait(false)(pas de contexte de synchronisation) - .NET 8 -> Explore
ConfigureAwaitOptionspour des cas avancés
Garde ça simple : si ton code doit revenir sur le thread UI, ne touche pas à ConfigureAwait !
Pour creuser le sujet, je vous recommande l’article (en anglais) de Stephen Cleary. ConfigureAwait in .NET 8
Async/Await - Meilleures pratiques en programmation asynchrone