Comment appeler une méthode asynchrone depuis du code synchrone en C# ? Du concret, pas de blabla
\n\nImagine : tu es le soir devant ton code, tu finalises un vieil utilitaire console en C#. Tout fonctionne de manière synchrone, comme une horloge. Et là — bam ! — tu dois appeler une API qui n'existe qu'en async Task. Pas question de réécrire tout le programme ? Une douleur familière, non ?
La question « How to call asynchronous method from synchronous method in C# » est l'une des plus populaires sur Stack Overflow. Et pour cause. Dans les commentaires, des tonnes de conseils allant de « fais .Result et ne t'inquiète pas » à « t'es fou, tu vas avoir un deadlock ! ». Décomposons tout cela : quelles approches fonctionnent vraiment, et lesquelles mènent en enfer.
\n\nPourquoi un simple « await » ne fonctionne-t-il pas ?
\n\nSi tu essaies d'écrire :
\npublic void SyncMethod()\n{\n await DoSomethingAsync(); // Erreur CS4032\n}\nLe compilateur refusera tout simplement de compiler. await ne peut être utilisé qu'à l'intérieur d'une méthode marquée du modificateur async. Et async void est une autre histoire que nous aborderons aussi.
Donc, nous avons une méthode asynchrone :
\npublic async Task<string> GetDataAsync()\n{\n using var httpClient = new HttpClient();\n return await httpClient.GetStringAsync("https://api.example.com/data");\n}\n\nEt une méthode synchrone à partir de laquelle nous voulons l'appeler. Passons en revue les options.
\n\nApproche 1 : GetAwaiter().GetResult() — le style hacker
\n\nLa méthode la plus populaire et sans doute la plus élégante :
\npublic string CallSync()\n{\n return GetDataAsync().GetAwaiter().GetResult();\n}\n\nPourquoi c'est cool :
\n- \n
- Ne génère pas d'exception
AggregateException(contrairement à.Result). Si une erreur survient dans la méthode asynchrone, tu obtiendras son type d'origine, pas un wrapper. \n - Le code se lit bien, sans try-catch superflu. \n
Mais il y a un hic : si ta méthode synchrone s'exécute dans un contexte avec SynchronizationContext (par exemple, dans un thread UI WinForms ou WPF), tu risques d'obtenir un deadlock — un blocage mutuel des threads. Le thread A attend l'exécution de la tâche asynchrone, et la tâche ne peut pas se terminer car elle attend que le thread A se libère. La galère.
Comment l'éviter : Utilise .ConfigureAwait(false) dans la méthode asynchrone elle-même. Mais cela modifie l'API, pas seulement le code appelant.
Approche 2 : Task.Run(async () => await ...).GetAwaiter() — déportation dans le pool de threads
\n\nSi tu ne peux pas modifier la méthode asynchrone mais que tu veux éviter à coup sûr le deadlock — déporte la tâche dans le pool de threads :
\npublic string CallSyncSafe()\n{\n return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();\n}\n\nLa magie : Task.Run lance le délégué sur un thread du ThreadPool, qui n'a pas de SynchronizationContext. Le deadlock est impossible. Même si la méthode originale utilise ConfigureAwait(true).
Inconvénient : Un changement de contexte (context switch) supplémentaire. Pour des centaines d'appels, ce n'est pas idéal. Pour des appels ponctuels, c'est parfait.
\n\nApproche 3 : .Result ou .Wait() — la méthode de grand-mère
\n\npublic string CallSyncOldSchool()\n{\n return GetDataAsync().Result; // Ou .Wait()\n}\n\nCe qui cloche :
\n- \n
- En cas d'erreur, lève une
AggregateException. Pour accéder à la véritable erreur, il faut fouiller dansInnerException. \n - Toujours le même risque de deadlock dans un contexte UI. \n
- Le code ressemble à une rustine de 2012. \n
Cette approche est un chiffon rouge pour la revue de code. Si je vois ça dans une pull request, je la refuse.
\n\nApproche 4 : AsyncHelper ou RunSync personnalisé
\n\nLes développeurs expérimentés écrivent parfois leurs propres helpers. Par exemple, celui-ci :
\n