Pourquoi devrais-je créer des opérations WebAPI asynchrones au lieu de celles de synchronisation?

109

J'ai l'opération suivante dans une API Web que j'ai créée:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public CartTotalsDTO GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh);
}

L'appel à ce service Web se fait via un appel Jquery Ajax de cette façon:

$.ajax({
      url: "/api/products/pharmacies/<%# Farmacia.PrimaryKeyId.Value.ToString() %>/page/" + vm.currentPage() + "/" + filter,
      type: "GET",
      dataType: "json",
      success: function (result) {
          vm.items([]);
          var data = result.Products;
          vm.totalUnits(result.TotalUnits);
      }          
  });

J'ai vu des développeurs qui implémentaient l'opération précédente de cette façon:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return await Task.Factory.StartNew(() => delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh));
}

Je dois dire, cependant, que GetProductsWithHistory () est une opération assez longue. Compte tenu de mon problème et de mon contexte, comment le fait de rendre l'opération webAPI asynchrone me sera-t-il bénéfique?

David Jiménez Martínez
la source
1
Le côté client utilise AJAX, qui est déjà asynchrone. Vous n'avez pas besoin que le service soit également écrit sous forme de fichier async Task<T>. Rappelez-vous, AJAX a été implémenté avant même que le TPL existe :)
Dominic Zukiewicz
65
Vous devez comprendre pourquoi vous implémentez des contrôleurs asynchrones, beaucoup ne le font pas. IIS a un nombre limité de threads disponibles et lorsque tous sont utilisés, le serveur ne peut pas traiter les nouvelles demandes. Avec les contrôleurs asynchrones, lorsqu'un processus attend la fin des E / S, son thread est libéré pour que le serveur l'utilise pour traiter d'autres requêtes.
Matija Grcic
3
Quels développeurs avez-vous vu faire cela? S'il y a un article de blog ou un article qui recommande cette technique, veuillez publier un lien.
Stephen Cleary
3
Vous ne bénéficiez pleinement de l'async que si votre processus prend en charge l'asynchrone depuis le haut (y compris l'application Web elle-même et vos contrôleurs) jusqu'à toutes les activités attendues en dehors de votre processus (y compris les retards de minuterie, les E / S de fichier, l'accès à la base de données, et requêtes Web qu'il fait). Dans ce cas, votre assistant délégué a besoin d'un GetProductsWithHistoryAsync()retour Task<CartTotalsDTO>. Il peut y avoir un avantage à écrire votre contrôleur de manière asynchrone si vous avez l'intention de migrer les appels qu'il effectue pour être également asynchrones; alors vous commencez à tirer parti des parties asynchrones au fur et à mesure que vous migrez le reste.
Keith Robertson
1
Si le processus que vous effectuez se déroule et touche la base de données, votre fil Web attend juste qu'il revienne et retienne ce fil. Si vous avez atteint votre nombre maximum de threads et qu'une autre demande arrive, elle doit attendre. Pourquoi faire ça? Au lieu de cela, vous voudriez libérer ce thread de votre contrôleur afin qu'une autre demande puisse l'utiliser et n'occuper un autre thread Web que lorsque votre demande d'origine de la base de données est revenue. msdn.microsoft.com/en-us/magazine/dn802603.aspx
user441521

Réponses:

98

Dans votre exemple spécifique, l'opération n'est pas du tout asynchrone, donc ce que vous faites est asynchrone sur synchronisation. Vous libérez juste un fil et en bloquez un autre. Il n'y a aucune raison à cela, car tous les threads sont des threads de pool de threads (contrairement à une application GUI).

Dans ma discussion sur «asynchrone sur synchro», j'ai fortement suggéré que si vous avez une API qui est implémentée en interne de manière synchrone, vous ne devriez pas exposer un homologue asynchrone qui encapsule simplement la méthode synchrone Task.Run.

De Dois-je exposer des wrappers synchrones pour les méthodes asynchrones?

Cependant, lors d'appels WebAPI asyncoù il y a une opération asynchrone réelle (généralement des E / S) au lieu de bloquer un thread qui se trouve et attend un résultat, le thread retourne au pool de threads et donc capable d'effectuer une autre opération. Dans l'ensemble, cela signifie que votre application peut faire plus avec moins de ressources et cela améliore l'évolutivité.

i3arnon
la source
3
@efaruk tous les threads sont des threads de travail. Libérer un thread ThreadPool et en bloquer un autre est inutile.
i3arnon
1
@efaruk Je ne suis pas sûr de ce que vous essayez de dire ... mais tant que vous êtes d'accord, il n'y a aucune raison d'utiliser async over sync dans WebAPI, alors tout va bien.
i3arnon
@efaruk "async over sync" (ie await Task.Run(() => CPUIntensive())) est inutile dans asp.net. Vous n'y gagnez rien. Vous libérez simplement un thread ThreadPool pour en occuper un autre. C'est moins efficace que d'appeler simplement la méthode synchrone.
i3arnon
1
@efaruk Non, ce n'est pas raisonnable. Votre exemple exécute les tâches indépendantes de manière séquentielle. Vous devez vraiment lire sur asyc / await avant de faire des recommandations. Vous auriez besoin d'utiliser await Task.WhenAllpour exécuter en parallèle.
Søren Boisen
1
@efaruk Comme l'explique Boisen, votre exemple n'ajoute aucune valeur en plus d'appeler ces méthodes synchrones de manière séquentielle. Vous pouvez l'utiliser Task.Runsi vous souhaitez paralléliser votre charge sur plusieurs threads, mais ce n'est pas ce que signifie «asynchronisation sur synchronisation». "async over sync" fait référence à la création d'une méthode async en tant que wrapper sur une méthode synchrone. Vous pouvez voir le dans la citation dans ma réponse.
i3arnon le
1

Une approche pourrait être (j'ai utilisé cela avec succès dans les applications client) d'avoir un service Windows exécutant les longues opérations avec les threads de travail, puis de le faire dans IIS pour libérer les threads jusqu'à ce que l'opération de blocage soit terminée: Remarque, cela suppose les résultats sont stockés dans une table (lignes identifiées par jobId) et un processus plus propre les nettoie quelques heures après utilisation.

Pour répondre à la question: "Compte tenu de mon problème et de mon contexte, en quoi le fait de rendre l'opération webAPI asynchrone me sera-t-il bénéfique?" étant donné que c'est "une opération assez longue", je pense plusieurs secondes plutôt que ms, cette approche libère les threads IIS. De toute évidence, vous devez également exécuter un service Windows qui lui-même prend des ressources, mais cette approche pourrait empêcher un flot de requêtes lentes de voler des threads à d'autres parties du système.

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
        var jobID = Guid.NewGuid().ToString()
        var job = new Job
        {
            Id = jobId,
            jobType = "GetProductsWithHistory",
            pharmacyId = pharmacyId,
            page = page,
            filter = filter,
            Created = DateTime.UtcNow,
            Started = null,
            Finished = null,
            User =  {{extract user id in the normal way}}
        };
        jobService.CreateJob(job);

        var timeout = 10*60*1000; //10 minutes
        Stopwatch sw = new Stopwatch();
        sw.Start();
        bool responseReceived = false;
        do
        {
            //wait for the windows service to process the job and build the results in the results table
            if (jobService.GetJob(jobId).Finished == null)
            {
                if (sw.ElapsedMilliseconds > timeout ) throw new TimeoutException();
                await Task.Delay(2000);
            }
            else
            {
                responseReceived = true;
            }
        } while (responseReceived == false);

    //this fetches the results from the temporary results table
    return jobService.GetProductsWithHistory(jobId);
}

la source