Appel de la méthode asynchrone de manière synchrone

231

J'ai une asyncméthode:

public async Task<string> GenerateCodeAsync()
{
    string code = await GenerateCodeService.GenerateCodeAsync();
    return code;
}

J'ai besoin d'appeler cette méthode à partir d'une méthode synchrone.

Comment puis-je faire cela sans avoir à dupliquer la GenerateCodeAsyncméthode pour que cela fonctionne de manière synchrone?

Mettre à jour

Pourtant, aucune solution raisonnable n'a été trouvée.

Cependant, je vois que HttpClientdéjà met en œuvre ce modèle

using (HttpClient client = new HttpClient())
{
    // async
    HttpResponseMessage responseAsync = await client.GetAsync(url);

    // sync
    HttpResponseMessage responseSync = client.GetAsync(url).Result;
}
Catalin
la source
1
J'espérais une solution plus simple, pensant que asp.net a géré cela beaucoup plus facilement que d'écrire autant de lignes de code
Catalin
Pourquoi ne pas simplement adopter le code asynchrone? Idéalement, vous voudriez plus de code asynchrone, pas moins.
Paulo Morgado
54
[Pourquoi ne pas simplement adopter le code asynchrone?] Ha, c'est peut-être précisément parce que l'on utilise le code asynchrone qu'ils ont besoin de cette solution car de grandes parties du projet sont converties! Vous ne pouvez pas reconstruire Rome en un jour.
Nicholas Petersen
1
@NicholasPetersen parfois une bibliothèque tierce peut vous forcer à le faire. Exemple de création de messages dynamiques dans la méthode WithMessage à partir de FluentValidation. Il n'y a pas d'API asynchrone pour cela en raison de la conception de la bibliothèque - les surcharges WithMessage sont statiques. D'autres méthodes de transmission d'arguments dynamiques à WithMessage sont étranges.
H.Wojtowicz

Réponses:

278

Vous pouvez accéder à la Resultpropriété de la tâche, ce qui entraînera le blocage de votre thread jusqu'à ce que le résultat soit disponible:

string code = GenerateCodeAsync().Result;

Remarque: Dans certains cas, cela peut entraîner un blocage: votre appel à Resultbloque le thread principal, empêchant ainsi l'exécution du reste du code asynchrone. Vous disposez des options suivantes pour vous assurer que cela ne se produit pas:

Cela ne signifie pas que vous devez simplement ajouter sans réfléchir .ConfigureAwait(false)après tous vos appels asynchrones! Pour une analyse détaillée sur pourquoi et quand vous devez utiliser .ConfigureAwait(false), consultez l'article de blog suivant:

Heinzi
la source
33
Si l'invocation resultrisque un blocage, alors quand est- il sûr d'obtenir le résultat? Est-ce que chaque appel asynchrone nécessite Task.Runou ConfigureAwait(false)?
Robert Harvey
4
Il n'y a pas de «thread principal» dans ASP.NET (contrairement à une application GUI), mais le blocage est toujours possible en raison de la AspNetSynchronizationContext.Post sérialisation des continuations asynchrones:Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
noseratio
4
@RobertHarvey: Si vous n'avez aucun contrôle sur la mise en œuvre de la méthode asynchrone sur laquelle vous bloquez, alors oui, vous devez la boucler Task.Runpour rester en sécurité. Ou utilisez quelque chose comme WithNoContextpour réduire le changement de thread redondant.
noseratio
10
REMARQUE: l'appel .Resultpeut toujours se bloquer si l'appelant se trouve sur le pool de threads lui-même. Prenez un scénario dans lequel le pool de threads est de taille 32 et 32 ​​tâches sont en cours d'exécution et Wait()/Resultattendent une 33e tâche encore à planifier qui veut s'exécuter sur l'un des threads en attente.
Warty
55

Vous devez obtenir le waiter ( GetAwaiter()) et mettre fin à l'attente de la fin de la tâche asynchrone ( GetResult()).

string code = GenerateCodeAsync().GetAwaiter().GetResult();
Diego Torres
la source
38
Nous avons rencontré des blocages à l'aide de cette solution. Être averti.
Oliver
6
MSDNTask.GetAwaiter : cette méthode est destinée à être utilisée par le compilateur plutôt qu'à être utilisée dans le code d'application.
foka
J'ai toujours le popup de dialogue d'erreur (contre ma volonté), avec les boutons 'Switch To' ou 'Retry'…. cependant, l'appel s'exécute et revient avec une réponse appropriée.
Jonathan Hansen
30

Vous devriez pouvoir faire cela en utilisant des délégués, expression lambda

private void button2_Click(object sender, EventArgs e)
    {

        label1.Text = "waiting....";

        Task<string> sCode = Task.Run(async () =>
        {
            string msg =await GenerateCodeAsync();
            return msg;
        });

        label1.Text += sCode.Result;

    }

    private Task<string> GenerateCodeAsync()
    {
        return Task.Run<string>(() => GenerateCode());
    }

    private string GenerateCode()
    {
        Thread.Sleep(2000);
        return "I m back" ;
    }
Faiyaz
la source
Cet extrait ne sera pas compilé. Le type de retour de Task.Run est Task. Voir ce blog MSDN pour une explication complète.
Appetere
5
Merci de l'avoir signalé, oui, il renvoie le type de tâche. Le remplacement de "string sCode" par la tâche <string> ou var sCode devrait le résoudre. Ajout d'un code de compilation complet pour plus de facilité.
Faiyaz
20

J'ai besoin d'appeler cette méthode à partir d'une méthode synchrone.

C'est possible avec GenerateCodeAsync().Resultou GenerateCodeAsync().Wait(), comme le suggère l'autre réponse. Cela bloquerait le thread actuel jusqu'à ce qu'il GenerateCodeAsyncsoit terminé.

Cependant, votre question est étiquetée avec , et vous avez également laissé le commentaire:

J'espérais une solution plus simple, pensant que asp.net a géré cela beaucoup plus facilement que d'écrire autant de lignes de code

Mon point est, vous ne devriez pas bloquer sur une méthode asynchrone dans ASP.NET. Cela réduira l'évolutivité de votre application Web et peut créer un blocage (lorsqu'une awaitsuite à l'intérieur GenerateCodeAsyncest publiée AspNetSynchronizationContext). Utiliser Task.Run(...).Resultpour décharger quelque chose sur un thread de pool puis bloquer bloquera encore plus l'évolutivité, car cela engendre +1 thread supplémentaire pour traiter une requête HTTP donnée.

ASP.NET prend en charge les méthodes asynchrones, soit via des contrôleurs asynchrones (dans ASP.NET MVC et API Web), soit directement via AsyncManageret PageAsyncTaskdans ASP.NET classique. Vous devez l'utiliser. Pour plus de détails, consultez cette réponse .

noseratio
la source
Je remplace la SaveChanges()méthode de DbContext, et ici j'appelle les méthodes asynchrones, donc malheureusement le contrôleur asynchrone ne m'aidera pas dans cette situation
Catalin
3
@RaraituL, en général, vous ne mélangez pas le code asynchrone et sync, choisissez le modèle euther. Vous pouvez implémenter les deux SaveChangesAsyncet SaveChangesassurez-vous simplement qu'ils ne sont pas appelés les deux dans le même projet ASP.NET.
noseratio
4
Par .NET MVCexemple IAuthorizationFilter, tous les filtres ne prennent pas en charge le code asynchrone , donc je ne peux pas utiliser asynctout le chemin
Catalin
3
@Noseratio qui est un objectif irréaliste. Il existe trop de bibliothèques avec du code asynchrone et synchrone ainsi que des situations dans lesquelles l'utilisation d'un seul modèle n'est pas possible. MVC ActionFilters ne prend pas en charge le code asynchrone, par exemple.
Justin Skiles
9
@Noserato, la question concerne l'appel de la méthode asynchrone à partir de synchrone. Parfois, vous ne pouvez pas modifier l'API que vous implémentez. Supposons que vous implémentiez une interface synchrone à partir d'un cadre tiers "A" (vous ne pouvez pas réécrire le cadre de manière asynchrone), mais que la bibliothèque tierce "B" que vous essayez d'utiliser dans votre implémentation a seulement asynchrone. Le produit résultant est également une bibliothèque et peut être utilisé n'importe où, y compris ASP.NET, etc.
dimzon
19

Microsoft Identity possède des méthodes d'extension qui appellent les méthodes asynchrones de manière synchrone. Par exemple, il existe une méthode GenerateUserIdentityAsync () et un égal CreateIdentity ()

Si vous regardez UserManagerExtensions.CreateIdentity (), cela ressemble à ceci:

 public static ClaimsIdentity CreateIdentity<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user,
        string authenticationType)
        where TKey : IEquatable<TKey>
        where TUser : class, IUser<TKey>
    {
        if (manager == null)
        {
            throw new ArgumentNullException("manager");
        }
        return AsyncHelper.RunSync(() => manager.CreateIdentityAsync(user, authenticationType));
    }

Voyons maintenant ce que fait AsyncHelper.RunSync

  public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        var cultureUi = CultureInfo.CurrentUICulture;
        var culture = CultureInfo.CurrentCulture;
        return _myTaskFactory.StartNew(() =>
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return func();
        }).Unwrap().GetAwaiter().GetResult();
    }

C'est donc votre wrapper pour la méthode asynchrone. Et s'il vous plaît, ne lisez pas les données de Result - cela bloquera potentiellement votre code dans ASP.

Il y a une autre façon - ce qui est suspect pour moi, mais vous pouvez aussi l'envisager

  Result r = null;

            YourAsyncMethod()
                .ContinueWith(t =>
                {
                    r = t.Result;
                })
                .Wait();
Vitaliy Markitanov
la source
3
Selon vous, quel est le problème avec la deuxième façon que vous avez suggérée?
David Clarke
@DavidClarke probablement le problème de sécurité des threads d'accéder à une variable non volatile à partir de plusieurs threads sans verrou.
Theodor Zoulias
9

Pour éviter les blocages, j'essaie toujours d'utiliser Task.Run() lorsque je dois appeler une méthode asynchrone de manière synchrone mentionnée par @Heinzi.

Cependant, la méthode doit être modifiée si la méthode asynchrone utilise des paramètres. Par exemple, Task.Run(GenerateCodeAsync("test")).Resultdonne l'erreur:

Argument 1: impossible de convertir de ' System.Threading.Tasks.Task<string>' en 'System.Action'

Cela pourrait être appelé comme ceci à la place:

string code = Task.Run(() => GenerateCodeAsync("test")).Result;
Ogglas
la source
6

La plupart des réponses sur ce sujet sont soit complexes, soit entraîner un blocage.

La méthode suivante est simple et elle évitera un blocage car nous attendons que la tâche se termine et que nous obtenions ensuite son résultat.

var task = Task.Run(() => GenerateCodeAsync()); 
task.Wait();
string code = task.Result;

En outre, voici une référence à un article MSDN qui parle exactement de la même chose - https://blogs.msdn.microsoft.com/jpsanders/2017/08/28/asp-net-do-not-use-task-result- dans-contexte-principal /

kamalpreet
la source
1

Je préfère une approche non bloquante:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While
Zibri
la source
0

Eh bien, j'utilise cette approche:

    private string RunSync()
    {
        var task = Task.Run(async () => await GenerateCodeService.GenerateCodeAsync());
        if (task.IsFaulted && task.Exception != null)
        {
            throw task.Exception;
        }

        return task.Result;
    }
Jiří Herník
la source
-1

L'autre façon pourrait être si vous voulez attendre la fin de la tâche:

var t = GenerateCodeService.GenerateCodeAsync();
Task.WhenAll(t);
string code = t.Result;
frablaser
la source
1
C'est tout à fait faux. WhenAll renvoie également une tâche que vous n'attendez pas.
Robert Schmidt
-1

ÉDITER:

La tâche a la méthode Wait, Task.Wait (), qui attend que la "promesse" soit résolue puis continue, la rendant ainsi synchrone. exemple:


async Task<String> MyAsyncMethod() { ... }

String mySyncMethod() {

    return MyAsyncMethod().Wait();
}
Avi Tshuva
la source
3
Veuillez développer votre réponse. Comment est-ce utilisé? Comment cela aide-t-il spécifiquement à répondre à la question?
Scratte
-2

Si vous avez une méthode asynchrone appelée " RefreshList ", vous pouvez appeler cette méthode asynchrone à partir d'une méthode non asynchrone comme ci-dessous.

Task.Run(async () => { await RefreshList(); });
dush88c
la source