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é

  1. Bibliothèque -> Utilise ConfigureAwait(false) partout
  2. Application -> N’utilise pas ConfigureAwait(false)
  3. ASP.NET Core > 2.0 -> Pas besoin de ConfigureAwait(false) (pas de contexte de synchronisation)
  4. .NET 8 -> Explore ConfigureAwaitOptions pour 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