web-dev-qa-db-fra.com

Envelopper du code synchrone dans un appel asynchrone

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 requête utilisateur, en fonction de l'état du cache et des 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.
L’appel synchrone au service ressemble donc à ce qui suit:

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

Et l'utilisation de la méthode est (note, cet appel de 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 l'implémentation de mon côté pour fournir un travail simultané de cette méthode, et voici ce à quoi je suis arrivé jusqu'ici.

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

Utilisation (une partie du code "do other stuff" s'exécute simultanément à l'appel du 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. Est-ce que j'utilise l'approche appropriée 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 cette méthode de manière asynchrone si c'est le seul appel que nous pourrions effectuer pour le moment (j'ai ce cas, lorsqu'il n'y a pas d'autres tâches à effectuer pendant l'attente de la fin)?
La plupart des articles du réseau traitant de ce sujet traitent de l'utilisation de async-await approche avec le code, qui fournit déjà awaitable méthodes, mais ce n'est pas mon cas. Here C'est l'article de Nice décrivant mon cas, qui ne décrit pas la situation des appels parallèles, décline l'option permettant d'appeler un appel à la synchronisation, mais à mon avis, ma situation est exactement l'occasion de le faire.
Merci d’avance pour votre aide et vos conseils.

75
Eadel

Il est important de faire la distinction entre deux types différents de concurrence. L'accès asynchrone correspond à plusieurs opérations asynchrones en vol (et comme chaque opération est asynchrone, aucune d'entre elles n'utilise réellement un thread). Parallèle La simultanéité 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 consiste à écrire une API asynchrone pour celui-ci.

Nous allons supposer que votre "service" est une opération liée à la CPU qui doit s'exécuter sur le même ordinateur que le serveur Web.

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

J'ai besoin de la demande pour exécuter plus rapidement.

Êtes-vous absolument certain que c'est ce que vous devez faire? Existe-t-il des modifications initiales que vous pouvez effectuer à la place, par exemple, lancer la demande et permettre à l'utilisateur de faire un autre travail pendant son traitement?

Je vais partir de l’hypothèse suivante: oui, vous devez vraiment exécuter la requête individuelle plus rapidement.

Dans ce cas, vous devrez exécuter du code parallèle sur votre serveur Web. Ceci est très certainement déconseillé en général, car le code parallèle utilisera des threads dont ASP.NET pourrait avoir besoin pour gérer d'autres requêtes, et en supprimant/ajoutant des threads, les heuristiques de pool de threads ASP.NET seraient supprimées. Cette décision a donc un impact sur l’ensemble de votre serveur.

Lorsque vous utilisez du code parallèle sur ASP.NET, vous prenez la décision de réellement limiter l’évolutivité de votre application Web. Vous risquez également de voir une quantité non négligeable de déséquilibres de threads, en particulier si vos demandes sont trop volumineuses. 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 en êtes à ce stade et que vous êtes certain de vouloir effectuer un traitement parallèle sur ASP.NET, vous disposez de plusieurs options.

Une des méthodes les plus faciles consiste à utiliser Task.Run, très similaire à votre code existant. Cependant, je ne recommande pas d'implémenter une méthode CalculateAsync car cela implique que le traitement est asynchrone (ce qui n'est pas le cas). Au lieu de cela, utilisez Task.Run au 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
}

Sinon, si cela fonctionne bien avec votre code, vous pouvez utiliser le type Parallel, c'est-à-dire, Parallel.For, Parallel.ForEach, ou Parallel.Invoke. L'avantage du code Parallel est que le thread de requête est utilisé comme un des threads parallèles, puis qu'il continue à s'exécuter dans le contexte du thread (il y a moins de changement de contexte que l'exemple async):

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

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

95
Stephen Cleary