Emballage du code synchrone dans un appel asynchrone

95

J'ai une méthode dans l'application ASP.NET, qui prend beaucoup de temps à compléter. Un appel à cette méthode peut se produire jusqu'à 3 fois au cours d'une demande utilisateur, selon l'état du cache et les paramètres fournis par l'utilisateur. Chaque appel prend environ 1 à 2 secondes. La méthode elle-même est un appel synchrone au service et il n'y a aucune possibilité de remplacer l'implémentation.
Ainsi, l'appel synchrone au service ressemble à ceci:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

Et l'utilisation de la méthode est (notez que l'appel de la méthode peut se produire plus d'une fois):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

J'ai essayé de changer la mise en œuvre de mon côté pour fournir un travail simultané de cette méthode, et voici ce à quoi je suis arrivé jusqu'à présent.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Utilisation (une partie du code "faire d'autres choses" s'exécute simultanément avec l'appel au service):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Ma question est la suivante. Dois-je utiliser la bonne approche pour accélérer l'exécution dans l'application ASP.NET ou est-ce que je fais un travail inutile en essayant d'exécuter du code synchrone de manière asynchrone? Quelqu'un peut-il expliquer pourquoi la deuxième approche n'est pas une option dans ASP.NET (si ce n'est vraiment pas le cas)? De plus, si une telle approche est applicable, dois-je appeler une telle méthode de manière asynchrone si c'est le seul appel que nous pourrions effectuer pour le moment (j'ai un tel cas, alors qu'il n'y a pas d'autre chose à faire en attendant la fin)?
La plupart des articles sur le net sur ce sujet couvrent l'utilisation de l' async-awaitapproche avec le code, qui fournit déjà des awaitableméthodes, mais ce n'est pas mon cas. Iciest le bel article décrivant mon cas, qui ne décrit pas la situation des appels parallèles, refusant l'option d'envelopper l'appel de synchronisation, mais à mon avis ma situation est exactement l'occasion de le faire.
Merci d'avance pour l'aide et les conseils.

Eadel
la source

Réponses:

116

Il est important de faire une distinction entre deux types différents de concurrence. La concurrence asynchrone se produit lorsque vous avez plusieurs opérations asynchrones en cours (et comme chaque opération est asynchrone, aucune d'elles n'utilise réellement un thread ). La concurrence parallèle se produit lorsque plusieurs threads effectuent chacun une opération distincte.

La première chose à faire est de réévaluer cette hypothèse:

La méthode elle-même est un appel synchrone au service et il n'y a aucune possibilité de remplacer l'implémentation.

Si votre "service" est un service Web ou tout autre élément lié aux E / S, la meilleure solution est d'écrire une API asynchrone pour celui-ci.

Je vais continuer avec l'hypothèse que votre "service" est une opération liée au processeur qui doit s'exécuter sur la même machine que le serveur Web.

Si tel est le cas, la prochaine chose à évaluer est une autre hypothèse:

J'ai besoin que la demande s'exécute plus rapidement.

Êtes-vous absolument sûr que c'est ce que vous devez faire? Y a-t-il des modifications frontales que vous pouvez apporter à la place - par exemple, démarrer la demande et permettre à l'utilisateur de faire d'autres travaux pendant son traitement?

Je partirai de l'hypothèse que oui, vous devez vraiment accélérer l'exécution de la demande individuelle.

Dans ce cas, vous devrez exécuter du code parallèle sur votre serveur Web. Cela n'est certainement pas recommandé en général, car le code parallèle utilisera des threads dont ASP.NET peut avoir besoin pour gérer d'autres demandes, et en supprimant / ajoutant des threads, il supprimera l'heuristique de threadpool ASP.NET. Donc, cette décision a un impact sur l'ensemble de votre serveur.

Lorsque vous utilisez du code parallèle sur ASP.NET, vous prenez la décision de vraiment limiter l'évolutivité de votre application Web. Vous pouvez également voir une bonne quantité de rotation des threads, surtout si vos requêtes sont en rafale. Je recommande d'utiliser uniquement du code parallèle sur ASP.NET si vous savez que le nombre d'utilisateurs simultanés sera assez faible (c'est-à-dire pas un serveur public).

Donc, si vous arrivez aussi loin et que vous êtes sûr de vouloir effectuer un traitement parallèle sur ASP.NET, vous avez alors quelques options.

L'une des méthodes les plus simples consiste à utiliser Task.Run, très similaire à votre code existant. Cependant, je ne recommande pas de mettre en œuvre une CalculateAsyncméthode car cela implique que le traitement est asynchrone (ce qui n'est pas le cas). Au lieu de cela, utilisez Task.Runau moment de l'appel:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Par ailleurs, si cela fonctionne bien avec votre code, vous pouvez utiliser le Paralleltype à savoir Parallel.For, Parallel.ForEachou Parallel.Invoke. L'avantage du Parallelcode est que le thread de demande est utilisé comme l'un des threads parallèles, puis reprend l'exécution dans le contexte du thread (il y a moins de changement de contexte que l' asyncexemple):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

Je ne recommande pas du tout d'utiliser Parallel LINQ (PLINQ) sur ASP.NET.

Stephen Cleary
la source
1
Merci beaucoup pour une réponse détaillée. Une dernière chose que je veux clarifier. Lorsque je commence l'exécution en utilisant Task.Run, le contenu est exécuté dans un autre thread, qui est extrait du pool de threads. Ensuite, il n'est pas du tout nécessaire d'envelopper les appels de blocage (comme le mien appel au service) dans Runs, car ils consommeront toujours un thread chacun, qui sera bloqué pendant l'exécution de la méthode. Dans une telle situation, le seul avantage qui reste async-awaitdans mon cas est d'effectuer plusieurs actions à la fois. Corrigez-moi si j'ai tort, s'il-vous plait.
Eadel
2
Oui, le seul avantage que vous tirez Run(ou Parallel) est la concurrence. Les opérations bloquent toujours chacune un thread. Puisque vous dites que le service est un service Web, je ne recommande pas d'utiliser Runou Parallel; à la place, écrivez une API asynchrone pour le service.
Stephen Cleary
3
@StephenCleary. qu'entendez-vous par "... et puisque chaque opération est asynchrone, aucune d'elles n'utilise réellement un thread ..." merci!
alltej
3
@alltej: Pour un code vraiment asynchrone, il n'y a pas de thread .
Stephen Cleary
4
@JoshuaFrank: Pas directement. Le code derrière une API synchrone doit bloquer le thread appelant, par définition. Vous pouvez utiliser une API asynchrone comme BeginGetResponseet en démarrer plusieurs, puis bloquer (de manière synchrone) jusqu'à ce qu'elles soient toutes complètes, mais ce type d'approche est délicat - voir blog.stephencleary.com/2012/07/dont-block-on -async-code.html et msdn.microsoft.com/en-us/magazine/mt238404.aspx . Il est généralement plus facile et plus propre à adopter asynccomplètement, si possible.
Stephen Cleary
1

J'ai trouvé que le code suivant peut convertir une tâche pour qu'elle s'exécute toujours de manière asynchrone

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

et je l'ai utilisé de la manière suivante

await ForceAsync(() => AsyncTaskWithNoAwaits())

Cela exécutera n'importe quelle tâche de manière asynchrone afin que vous puissiez les combiner dans des scénarios WhenAll, WhenAny et d'autres utilisations.

Vous pouvez également simplement ajouter le Task.Yield () comme première ligne de votre code appelé.

kernowcode
la source