1351 votes

Comment appeler une méthode asynchrone à partir d'une méthode synchrone en C#?

J'ai une méthode public async Task Foo() que je veux appeler depuis une méthode synchrone. Jusqu'à présent, tout ce que j'ai vu dans la documentation de MSDN consiste à appeler des méthodes async via des méthodes async, mais tout mon programme n'est pas construit avec des méthodes async.

Est-ce même possible ?

Voici un exemple d'appel de ces méthodes depuis une méthode asynchrone :
Didacticiel : Accéder au Web en utilisant Async et Await (C# et Visual Basic)

Maintenant, je regarde comment appeler ces méthodes async depuis des méthodes synchrones.

4 votes

Je suis également tombé sur ce problème. En remplaçant un RoleProvider, vous ne pouvez pas modifier la signature de la méthode GetRolesForUser pour la rendre asynchrone. Vous ne pouvez donc pas utiliser await pour appeler de manière asynchrone une API. Ma solution temporaire a été d'ajouter des méthodes synchrones à ma classe HttpClient générique, mais j'aimerais savoir si cela est possible (et quelles pourraient en être les implications).

3 votes

Parce que votre méthode async void Foo() ne renvoie pas de Task, cela signifie qu'un appelant ne peut pas savoir quand elle se termine, elle doit plutôt renvoyer Task.

1 votes

En créant un lien vers une question / réponse associée sur la façon de le faire sur un thread UI.

1069voto

Stephen Cleary Points 91731

La programmation asynchrone se "propage" à travers la base de code. Il a été comparé à un virus zombie. La meilleure solution est de le laisser se propager, mais parfois ce n'est pas possible.

J'ai écrit quelques types dans ma bibliothèque Nito.AsyncEx pour gérer une base de code partiellement asynchrone. Il n'y a pas de solution qui fonctionne dans toutes les situations, cependant.

Solution A

Si vous avez une méthode asynchrone simple qui n'a pas besoin de se synchroniser avec son contexte, alors vous pouvez utiliser Task.WaitAndUnwrapException:

var task = MaMethodeAsynchrone();
var result = task.WaitAndUnwrapException();

Vous ne voulez pas utiliser Task.Wait ou Task.Result car ils encapsulent les exceptions dans AggregateException.

Cette solution est appropriée uniquement si MaMethodeAsynchrone ne se synchronise pas avec son contexte. En d'autres termes, chaque await dans MaMethodeAsynchrone devrait se terminer par ConfigureAwait(false). Cela signifie qu'elle ne peut pas mettre à jour les éléments de l'interface utilisateur ou accéder au contexte de requête ASP.NET.

Solution B

Si MaMethodeAsynchrone doit se synchroniser avec son contexte, alors vous pouvez peut-être utiliser AsyncContext.RunTask pour fournir un contexte imbriqué:

var result = AsyncContext.RunTask(MaMethodeAsynchrone).Result;

*Mise à jour du 14/04/2014 : Dans les versions plus récentes de la bibliothèque, l'API est la suivante :

var result = AsyncContext.Run(MaMethodeAsynchrone);

(Il est possible d'utiliser Task.Result dans cet exemple car RunTask va propager les exceptions de Task).

La raison pour laquelle vous pourriez avoir besoin de AsyncContext.RunTask au lieu de Task.WaitAndUnwrapException

  1. Une méthode synchrone appelle une méthode asynchrone, obtenant une Task.
  2. La méthode synchrone effectue un blocage sur la Task.
  3. La méthode async utilise await sans ConfigureAwait.
  4. La Task ne peut pas se terminer dans cette situation car elle ne se termine que lorsque la méthode async est finie ; la méthode async ne peut pas se terminer car elle tente de planifier sa continuation vers le SynchronizationContext, et WinForms/WPF/SL/ASP.NET ne permettra pas à la continuation de s'exécuter car la méthode synchrone est déjà en cours d'exécution dans ce contexte.

C'est une des raisons pour lesquelles il est judicieux d'utiliser ConfigureAwait(false) dans chaque méthode async autant que possible.

Solution C

AsyncContext.RunTask ne fonctionnera pas dans tous les scénarios. Par exemple, si la méthode async attend quelque chose qui nécessite un événement d'interface utilisateur pour se terminer, alors vous serez confronté à un blocage même avec le contexte imbriqué. Dans ce cas, vous pourriez démarrer la méthode async sur le thread pool :

var task = Task.Run(async () => await MaMethodeAsynchrone());
var result = task.WaitAndUnwrapException();

Cependant, cette solution nécessite une MaMethodeAsynchrone qui fonctionnera dans le contexte du thread pool. Elle ne pourra donc pas mettre à jour les éléments de l'interface utilisateur ou accéder au contexte de requête ASP.NET. Et dans ce cas, vous pourriez aussi bien ajouter ConfigureAwait(false) à ses déclarations await, et utiliser la solution A.

Mise à jour : Article de MSDN datant de 2015 'Programmation asynchrone - Développement asynchrone Brownfield' par Stephen Cleary.

22 votes

La solution A semble correspondre à ce que je veux, mais il semble que task.WaitAndUnwrapException() n'a pas été intégré dans le .Net 4.5 RC; il a seulement task.Wait(). Avez-vous une idée de comment faire cela avec la nouvelle version ? Ou s'agit-il d'une méthode d'extension personnalisée que vous avez écrite ?

6 votes

WaitAndUnwrapException est ma propre méthode de ma bibliothèque AsyncEx. Les bibliothèques officielles .NET ne fournissent pas beaucoup d'aide pour mélanger du code synchrone et asynchrone (et en général, vous ne devriez pas le faire !). J'attends la sortie de .NET 4.5 RTW et l'achat d'un nouvel ordinateur portable non-XP avant de mettre à jour AsyncEx pour qu'il fonctionne sous 4.5 (je ne peux actuellement pas développer pour 4.5 car je suis bloqué sur XP pour quelques semaines encore).

0 votes

J'ai essayé d'utiliser Nito.AsyncEx, mais la méthode Task me donne une erreur d'ambiguïté... (Elle n'a pas RunkTask) Comment puis-je le résoudre?

308voto

Erik Philips Points 18156

Microsoft a construit une classe AsyncHelper (interne) pour exécuter Async comme Sync. Le code source ressemble à:

internal static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new 
      TaskFactory(CancellationToken.None, 
                  TaskCreationOptions.None, 
                  TaskContinuationOptions.None, 
                  TaskScheduler.Default);

    public static TResult RunSync(Func> func)
    {
        return AsyncHelper._myTaskFactory
          .StartNew>(func)
          .Unwrap()
          .GetAwaiter()
          .GetResult();
    }

    public static void RunSync(Func func)
    {
        AsyncHelper._myTaskFactory
          .StartNew(func)
          .Unwrap()
          .GetAwaiter()
          .GetResult();
    }
}

Les classes de base de Microsoft.AspNet.Identity ont uniquement des méthodes Async et pour les appeler comme Sync, il existe des classes avec des méthodes d'extension qui ressemblent à (exemple d'utilisation):

public static TUser FindById(this UserManager manager, TKey userId) where TUser : class, IUser where TKey : IEquatable
{
    if (manager == null)
    {
        throw new ArgumentNullException("manager");
    }
    return AsyncHelper.RunSync(() => manager.FindByIdAsync(userId));
}

public static bool IsInRole(this UserManager manager, TKey userId, string role) where TUser : class, IUser where TKey : IEquatable
{
    if (manager == null)
    {
        throw new ArgumentNullException("manager");
    }
    return AsyncHelper.RunSync(() => manager.IsInRoleAsync(userId, role));
}

Pour ceux qui se préoccupent des termes de licence du code, voici un lien vers un code très similaire (ajoute juste le support de la culture sur le thread) qui indique qu'il est sous licence MIT par Microsoft. https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

Cela ne serait-il pas la même chose que d'appeler simplement Task.Run(async () => await AsyncFunc()).Result? À ma connaissance, Microsoft ne recommande désormais pas d'appeler TaskFactory.StartNew, car ils sont équivalents et l'un est plus lisible que l'autre.

Absolument pas.

La réponse simple est que

.Unwrap().GetAwaiter().GetResult() != .Result

Tout d'abord, le

Est-ce que Task.Result est la même chose que .GetAwaiter.GetResult()?

Deuxièmement, .Unwrap() fait en sorte que la configuration de la tâche ne bloque pas la tâche enveloppée.

Ce qui devrait inciter quiconque à se demander

Cela ne serait-il pas la même chose que d'appeler simplement Task.Run(async () => await AsyncFunc()).GetAwaiter().GetResult()

Ce qui serait alors un Cela dépend.

En ce qui concerne l'utilisation de Task.Start(), Task.Run() et Task.Factory.StartNew()

Extrait:

Task.Run utilise TaskCreationOptions.DenyChildAttach ce qui signifie que les tâches enfant ne peuvent pas être attachées au parent et il utilise TaskScheduler.Default ce qui signifie que celui qui exécute les tâches sur le pool de threads sera toujours utilisé pour exécuter les tâches.

Task.Factory.StartNew utilise TaskScheduler.Current ce qui signifie le planificateur du thread actuel, il pourrait être TaskScheduler.Default mais pas toujours.

Lecture supplémentaire:

Spécification d'un contexte de synchronisation

Contexte de synchronisation ASP.NET Core

Pour plus de sécurité, ne serait-il pas mieux de l'appeler ainsi AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false)); De cette manière, nous disons à la méthode "interne" "s'il vous plaît ne pas essayer de synchroniser avec le contexte supérieur et débloquer"

Point vraiment important de alex-from-jitbit et comme la plupart des questions architecturales d'objets cela dépend.

En tant que méthode d'extension, voulez-vous imposer cela pour absolument chaque appel, ou laissez-vous le programmeur utiliser la fonction configurer cela sur ses propres appels Async? Je pourrais voir un cas d'utilisation pour appeler trois scénarios; cela a probablement plus de sens dans la plupart des cas, mais compte tenu qu'il n'y a pas de Contexte dans ASP.Net Core si vous pouviez garantir que c'était par exemple interne pour un ASP.Net Core, alors cela n'aurait pas d'importance.

4 votes

Mes méthodes asynchrones attendent d'autres méthodes asynchrones. Je ne décore PAS l'un de mes appels await avec ConfigureAwait(false). J'ai essayé d'utiliser AsyncHelper.RunSync pour appeler une fonction asynchrone à partir de la fonction Application_Start() dans Global.asax et cela semble fonctionner. Cela signifie-t-il que AsyncHelper.RunSync n'est pas fiablement sujet au problème de blocage "retour à contexte de l'appelant" dont j'ai lu ailleurs dans cette publication?

1 votes

@Bob.at.SBS dépend de ce que votre code fait. Ce n'est pas aussi simple que si j'utilise ce code, suis-je en sécurité. C'est une façon très minimaliste et semi-sûre d'exécuter des commandes asynchrones de manière synchrone, mais il peut être facilement utilisé de manière inappropriée pour provoquer des impasses.

1 votes

Merci. 2 questions de suivi : 1) Pouvez-vous donner un exemple de quelque chose que la méthode async veut éviter qui pourrait causer un deadlock, et 2) les deadlocks dans ce contexte sont-ils souvent dépendants du timing ? Si ça fonctionne en pratique, pourrais-je quand même avoir un deadlock dépendant du timing qui se cache dans mon code ?

76voto

NStuke Points 21

Je ne suis pas sûr à 100%, mais je crois que la technique décrite dans ce blog devrait fonctionner dans de nombreuses circonstances :

Vous pouvez donc utiliser task.GetAwaiter().GetResult() si vous souhaitez invoquer directement cette logique de propagation.

12 votes

La solution A dans la réponse de Stephen Cleary ci-dessus utilise cette méthode. Voir la source de WaitAndUnwrapException.

0 votes

Avez-vous besoin d'utiliser GetResult() si la fonction que vous appelez est void ou task? Je veux dire, si vous ne voulez pas obtenir de résultats en retour

1 votes

Oui, sinon cela ne sera pas bloqué jusqu'à la fin de la tâche. Autrement, au lieu d'appeler GetAwaiter().GetResult(), vous pouvez appeler .Wait()

68voto

Despertar Points 5365
public async Task StartMyTask()
{
    await Foo()
    // code to execute once foo is done
}

static void Main()
{
     var myTask = StartMyTask(); // appeler votre méthode qui renverra le contrôle une fois qu'il atteindra l'attente
     // maintenant vous pouvez continuer à exécuter du code ici
     string result = myTask.Result; // attendre que la tâche se termine pour continuer
     // utiliser le résultat
}

Vous pouvez lire le mot-clé 'await' comme "démarrer cette tâche longue, puis renvoyer le contrôle à la méthode appelante". Une fois la tâche longue terminée, elle exécute ensuite le code après elle. Le code après l'attente est similaire à ce qui était utilisé pour les méthodes de rappel. La grande différence étant que le flux logique n'est pas interrompu, ce qui rend beaucoup plus facile à écrire et à lire.

24 votes

Wait enveloppe les exceptions et a la possibilité d'un deadlock.

0 votes

Je pensais que si vous appelez une méthode asynchrone sans utiliser await, elle serait exécutée de manière synchrone. En tout cas, cela fonctionne pour moi (sans appeler myTask.Wait). En fait, j'ai obtenu une exception lorsque j'ai essayé d'appeler myTask.RunSynchronously() car elle avait déjà été exécutée !

0 votes

Note : Vous pouvez ensuite obtenir le résultat de type T en appelant myTask.Result() après le Wait()

15voto

base2 Points 523

Vous pouvez appeler n'importe quelle méthode asynchrone à partir de code synchrone, c'est-à-dire jusqu'à ce que vous deviez await sur elles, auquel cas elles doivent également être marquées comme étant async.

Comme beaucoup de gens le suggèrent ici, vous pourriez appeler Wait() ou Result sur la tâche résultante dans votre méthode synchrone, mais alors vous vous retrouvez avec un appel bloquant dans cette méthode, ce qui va à l'encontre du concept de l'asynchronicité.

Si vraiment vous ne pouvez pas rendre votre méthode async et que vous ne voulez pas bloquer la méthode synchrone, alors vous devrez utiliser une méthode de rappel en la passant en paramètre à la méthode ContinueWith() sur la tâche.

11 votes

Alors ce ne serait pas appeler la méthode de manière synchrone maintenant, n'est-ce pas?

6 votes

Tel que je le comprends, la question était : pouvez-vous appeler une méthode asynchrone depuis une méthode non asynchrone. Cela n'implique pas nécessairement d'appeler la méthode asynchrone de manière bloquante.

2 votes

Désolé, votre "ils doivent également être marqués async" a attiré mon attention loin de ce que vous disiez vraiment.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X