CodeGym /Cours /C# SELF /Interaction du code asynchrone et synchrone

Interaction du code asynchrone et synchrone

C# SELF
Niveau 62 , Leçon 4
Disponible

1. Introduction

L'asynchronisme en C# est un outil puissant. Mais parfois il faut faire face à une situation où du code asynchrone doit être appelé depuis du synchrone (ou inversement). On pourrait penser que tout devrait "magiquement" fonctionner, mais en pratique il peut y avoir des blocages étranges (deadlock), une perte de performance et même, de façon inattendue… une interface utilisateur cassée. Et souvent le bug n'apparaît que sur des données réelles : en production ou chez l'utilisateur. La cause est souvent une mauvaise interaction entre le code synchrone et asynchrone et des détails non évidents du fonctionnement du planificateur de tâches .NET.

Aussi, les bibliothèques et frameworks modernes poussent à utiliser activement des méthodes asynchrones, et vous devez comprendre comment "coller" proprement du code asynchrone dans des chaînes synchrones existantes ou, au contraire, comment appeler correctement du code synchrone depuis une méthode asynchrone.

Rappel rapide : que se passe-t-il quand on fait await ?

Quand vous écrivez :

await SomeAsyncMethod();

Le code est "scindé" en deux parties : avant le await et après. La première partie s'exécute jusqu'à la première attente asynchrone (par exemple une requête réseau), et la suite s'exécute après sa complétion. Question : où s'exécutera la "suite" ? Sur le même thread ? Sur un autre ? Et si on écrit une application desktop (par ex. WPF ou WinForms), ou bien une console ? La réponse — ça dépend. Et ici intervient, par exemple, ConfigureAwait.

Qu'est-ce que le SynchronizationContext ?

Le SynchronizationContext est un mécanisme spécial de .NET qui permet au code de "mémoriser" où et comment la continuation d'une opération asynchrone doit être invoquée.

  • Dans les applications classiques WinForms/WPF, le SynchronizationContext garantit qu'après le await la partie restante de la méthode continuera à s'exécuter sur le même thread que l'UI, afin d'éviter les erreurs "accès au contrôle depuis un thread incorrect".
  • Dans ASP.NET (ancien, pas Core) le SynchronizationContext permet de restaurer le HttpContext et de continuer à travailler avec la requête HTTP.
  • Dans les applications console et ASP.NET Core, le SynchronizationContext est en général absent (égale à null), et les continuations s'exécutent sur le thread pool.

Qu'est-ce que le TaskScheduler ?

Le TaskScheduler est un mécanisme de plus bas niveau. Dans la plupart des cas vous travaillez avec TaskScheduler.Default, qui utilise le thread pool .NET. Il est responsable de quand et où les tâches sont exécutées.

2. Deadlock en mélangeant await et Result/Wait()

Une des plus célèbres pièges en C# :

// Quelque part dans le code UI
var result = SomeAsyncMethod().Result;

ou

SomeAsyncMethod().Wait();

Et voilà : l'application "bloque". Pourquoi ?

Comment ça se produit ?

  1. Vous appelez une méthode asynchrone et vous demandez immédiatement .Result ou .Wait() — c'est-à-dire que vous "bloquez" le thread courant en attendant la fin de la tâche asynchrone.
  2. SomeAsyncMethod fait un await à l'intérieur, et planifie la continuation sur le même thread via le SynchronizationContext, mais ce thread est déjà bloqué en attendant le Result/.Wait().
  3. Comme le thread attend la fin du Result/.Wait(), il ne peut pas exécuter la continuation.
  4. Tout est bloqué, deadlock : le thread attend lui-même.

C'est particulièrement facile à reproduire dans les applications UI, où tout le code s'exécute sur le thread d'interface et toutes les continuations attendent ce thread. Dans les applications console et ASP.NET Core (sans SynchronizationContext) ces deadlocks sont rares.

Blague de terrain : Si vous avez réussi à obtenir un deadlock avec .Result — félicitations, vous êtes à deux pas du titre de Senior :D

3. Comment intégrer correctement du code asynchrone dans du synchrone ?

Recommandation n°1 : asynchronisme de haut en bas

Si vous avez une méthode asynchrone, remontez le async/await vers le haut de la pile d'appels jusqu'à l'UI ou le point d'entrée. Ne conservez pas l'asynchronisme "à mi-chemin".

Mauvais (bloque le thread) :

// Une méthode synchrone appelle une asynchrone via .Result
public void DoStuff()
{
    var data = GetDataAsync().Result;
    // ...
}

Bien (asynchronisme traversant) :

public async Task DoStuffAsync()
{
    var data = await GetDataAsync();
    // ...
}

Si possible — utilisez toujours await, et non .Result / .Wait().

4. Mais il y a des situations : il faut appeler async depuis sync

Réécrire toute la chaîne en async (meilleure option si vous avez le choix).

Utiliser des patterns spéciaux : par exemple, démarrer la tâche sur un thread séparé via Task.Run, et appeler la méthode async à l'intérieur.

public void DoStuff()
{
    var result = Task.Run(() => SomeAsyncMethod()).Result;
}

Mais : il y a aussi des nuances avec la synchronisation dans les applications UI, donc mieux vaut éviter cela sans nécessité absolue.

5. Que fait ConfigureAwait(false) ?

Parfois vous n'avez pas besoin que le code après le await continue à s'exécuter sur le même thread (par ex. dans des applications serveur ou dans une bibliothèque où le SynchronizationContext n'a pas d'importance). Au contraire, il est préférable que .NET ne se rattache pas à un thread particulier — ça accélère le travail !

Syntaxe et principe

await SomeAsyncMethod().ConfigureAwait(false);
  • ConfigureAwait(false) dit : "je n'ai pas besoin du SynchronizationContext natif, continue l'exécution n'importe où, même sur un autre thread".
  • ConfigureAwait(true) (par défaut) — "continue l'exécution là où l'await a été appelé, de préférence dans le même SynchronizationContext".

Schéma visuel


        ┌─────────────────────────────────────────────────────┐
        │                     SynchronizationContext          │
        └─────────────────────────────────────────────────────┘
                      ↑                             ↑
       (UI-thread)   await SomeAsyncMethod()        Continuation (après await)
                  ──────────────────────────────>  (le même thread — si ConfigureAwait(true))
                      ↓
                    (n'importe quel thread — si ConfigureAwait(false))

Exemple d'utilisation de ConfigureAwait(false) — code "bibliothèque"

Imaginez que vous écrivez une bibliothèque utilisée par n'importe qui : WinForms, WPF, ASP.NET, applications console…

Vous ne devez pas dépendre de leur modèle de threads. Donc utilisez toujours ConfigureAwait(false) dans les méthodes asynchrones de votre bibliothèque :

public async Task<string> LoadDataFromUrlAsync(string url)
{
    using var client = new HttpClient();
    string content = await client.GetStringAsync(url).ConfigureAwait(false);
    return content;
}

Maintenant votre méthode ne "demandera" pas d'exécution sur un SynchronizationContext spécifique. C'est plus sûr et plus performant (moins de switches de threads).

Exemple : que se passe-t-il avec await sans ConfigureAwait

Considérons une application WPF :

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Button1.Content = "Chargement...";
    await Task.Delay(2000);  // Simulation d'une opération longue
    Button1.Content = "Terminé!";
}

Task.Delay fait un "await" à l'intérieur. Par défaut après le await le contrôle revient sur le thread UI pour pouvoir continuer à mettre à jour les contrôles.

Si à l'intérieur de votre opération longue vous utilisez ConfigureAwait(false) :

await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "Terminé!";  // Erreur!

Une exception apparaîtra : InvalidOperationException : "The calling thread cannot access this object because a different thread owns it."
Parce que la "continuation" s'exécute sur un thread différent, et on ne peut pas accéder à l'UI depuis ce thread.

Conclusion : utilisez ConfigureAwait(false) seulement là où l'accès à l'UI / au contexte n'est pas nécessaire.

6. Nuances utiles

Où et quand utiliser ConfigureAwait

Scénario Faut-il utiliser .ConfigureAwait(false) ? Pourquoi ?
Code de bibliothèque Oui Appelé dans n'importe quel contexte, l'UI n'est pas nécessaire
Dans le code ASP.NET Core Oui (le contexte est presque absent) Améliore la performance
Dans WinForms/WPF, quand on touche l'UI Non Il faut revenir au thread UI
Méthodes synchrones Sans objet Il n'y a pas de SynchronizationContext
Applications console Possible, mais on ne verra pas l'effet Il n'y a pas de contexte

Comment savoir s'il y a un SynchronizationContext ?

Vous pouvez le vérifier directement depuis le code :

Console.WriteLine(SynchronizationContext.Current == null
    ? "Pas de contexte"
    : "Le contexte existe");
  • Dans les applications console et ASP.NET Core vous obtiendrez "Pas de contexte".
  • Dans WinForms/WPF — "Le contexte existe".

Visualisation des transitions de threads

sequenceDiagram
    participant MainThread as Thread principal (UI/Console)
    participant ThreadPool as Thread du pool

    MainThread->>SomeAsyncMethod: Appel de la méthode
    SomeAsyncMethod->>MainThread: await sans ConfigureAwait
    Note right of MainThread: Après await on revient sur le même thread
    SomeAsyncMethod->>ThreadPool: await avec ConfigureAwait(false)
    Note right of ThreadPool: Après await on peut travailler sur n'importe quel thread

Règles courtes d'utilisation

  • N'utilisez pas .Result et .Wait() dans le code UI avec des méthodes asynchrones.
  • Dans le code de bibliothèque, utilisez systématiquement ConfigureAwait(false) pour tous les await.
  • Dans les applications UI, utilisez ConfigureAwait(false) seulement à l'intérieur de méthodes qui n'accèdent pas aux éléments d'interface.
  • Mieux vaut ne pas mélanger code synchrone et asynchrone sans nécessité.
  • Si vous devez appeler async depuis sync — réfléchissez à deux fois : ne peut-on pas rendre toute la chaîne async ?

7. Erreurs typiques des débutants avec l'asynchronisme

Erreur n°1 : usage généralisé de .Result ou .Wait().
Très souvent les développeurs, surtout après quelques articles sur l'asynchronisme, commencent à ajouter .Result ou .Wait() partout pour "synchroniser" les appels asynchrones. À première vue c'est pratique, mais en pratique c'est le chemin garanti vers des deadlocks dans les applications UI. C'est particulièrement dangereux si à l'intérieur des méthodes asynchrones on n'utilise pas ConfigureAwait(false) — le thread UI se bloque et ne peut pas exécuter la continuation de la tâche.

Erreur n°2 : utilisation incorrecte ou excessive de ConfigureAwait(false).
Certains débutants appliquent ConfigureAwait(false) partout, même dans du code qui travaille avec des éléments d'UI. Dans les applications UI cela mène à des erreurs d'accès aux contrôles (InvalidOperationException), puisque la continuation s'exécute maintenant sur un thread différent du thread UI.

Erreur n°3 : oublier que ConfigureAwait fonctionne seulement avec des objets awaitable.
Beaucoup pensent qu'on peut utiliser ConfigureAwait(false) pour n'importe quelle méthode, mais en réalité cela ne fonctionne qu'avec des objets retournant Task, Task<T>, ValueTask et d'autres types awaitable. Pour les méthodes synchrones ConfigureAwait n'a aucun effet, et l'attente de telles méthodes ne devient pas asynchrone.

Erreur n°4 : mélange synchro/async sans nécessité.
Les tentatives d'appeler une méthode asynchrone depuis du code synchrone sans stratégie réfléchie (par ex. via .Result, .Wait() ou Task.Run) mènent souvent à des bugs subtils, perte de performance et deadlocks difficiles à diagnostiquer. Même si la méthode semble courte et sûre, de tels appels dans la chaîne peuvent casser le fonctionnement de toute l'application.

Erreur n°5 : sous-estimer l'impact du SynchronizationContext.
Les débutants oublient souvent que la continuation après un await peut s'exécuter sur le même thread (UI) si on n'utilise pas ConfigureAwait(false). Cela conduit à des comportements imprévisibles, surtout en combinant du code UI et du code de bibliothèque où attentes et continuations s'entremêlent.

2
Mission
C# SELF, niveau 62, leçon 4
Bloqué
Appel d'une méthode asynchrone depuis une méthode synchrone
Appel d'une méthode asynchrone depuis une méthode synchrone
1
Étude/Quiz
Flux de données asynchrones, niveau 62, leçon 4
Indisponible
Flux de données asynchrones
Plongée profonde dans l'asynchronicité
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION