Warum sollte ich asynchrone WebAPI-Vorgänge erstellen, anstatt solche zu synchronisieren?

109

Ich habe die folgende Operation in einer von mir erstellten Web-API:

// 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);
}

Der Aufruf dieses Webservices erfolgt über einen Jquery Ajax-Aufruf auf folgende Weise:

$.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);
      }          
  });

Ich habe einige Entwickler gesehen, die die vorherige Operation folgendermaßen implementieren:

// 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));
}

Ich muss jedoch sagen, dass GetProductsWithHistory () eine ziemlich lange Operation ist. Wie kann ich angesichts meines Problems und meines Kontextes davon profitieren, wenn ich die webAPI-Operation asynchron mache?

David Jiménez Martínez
quelle
1
Die Clientseite verwendet AJAX, das bereits asynchron ist. Sie müssen den Dienst nicht auch als schreiben async Task<T>. Denken Sie daran, AJAX wurde implementiert, bevor die TPL überhaupt existierte :)
Dominic Zukiewicz
65
Sie müssen verstehen, warum Sie asynchrone Controller implementieren, viele nicht. IIS verfügt über eine begrenzte Anzahl verfügbarer Threads. Wenn alle verwendet werden, kann der Server keine neuen Anforderungen verarbeiten. Bei asynchronen Controllern wird, wenn ein Prozess auf den Abschluss der E / A wartet, sein Thread freigegeben, damit der Server andere Anforderungen verarbeiten kann.
Matija Grcic
3
Welche Entwickler haben Sie gesehen, die das tun? Wenn es einen Blog-Beitrag oder Artikel gibt, der diese Technik empfiehlt, posten Sie bitte einen Link.
Stephen Cleary
3
Sie profitieren nur dann voll von Async, wenn Ihr Prozess von oben (einschließlich der Webanwendung selbst und Ihrer Controller) bis zu wartbaren Aktivitäten außerhalb Ihres Prozesses (einschließlich Timer-Verzögerungen, Datei-E / A, DB-Zugriff) asynchron ist. und Webanfragen, die es macht). In diesem Fall benötigt Ihr Delegiertenhelfer eine GetProductsWithHistoryAsync()Rückgabe Task<CartTotalsDTO>. Das asynchrone Schreiben Ihres Controllers kann von Vorteil sein, wenn Sie die Aufrufe migrieren möchten, die auch asynchron sind. Dann profitieren Sie von den asynchronen Teilen, während Sie den Rest migrieren.
Keith Robertson
1
Wenn der Prozess, den Sie ausführen, abläuft und auf die Datenbank trifft, wartet Ihr Web-Thread nur darauf, dass er zurückkommt und diesen Thread hält. Wenn Sie Ihre maximale Thread-Anzahl erreicht haben und eine weitere Anfrage eingeht, muss sie warten. Warum das tun? Stattdessen möchten Sie diesen Thread von Ihrem Controller freigeben, damit eine andere Anforderung ihn verwenden und erst dann einen anderen Web-Thread aufnehmen kann, wenn Ihre ursprüngliche Anforderung aus der Datenbank zurückkommt. msdn.microsoft.com/en-us/magazine/dn802603.aspx
user441521

Antworten:

98

In Ihrem speziellen Beispiel ist der Vorgang überhaupt nicht asynchron. Sie tun also asynchron über synchron. Sie geben nur einen Thread frei und blockieren einen anderen. Es gibt keinen Grund dafür, da alle Threads Thread-Pool-Threads sind (im Gegensatz zu einer GUI-Anwendung).

In meiner Diskussion über "Async over Sync" habe ich dringend empfohlen, dass Sie, wenn Sie eine API haben, die intern synchron implementiert ist, kein asynchrones Gegenstück verfügbar machen sollten, das die synchrone Methode einfach einschließt Task.Run.

Von Soll ich synchrone Wrapper für asynchrone Methoden verfügbar machen?

Wenn Sie jedoch WebAPI-Aufrufe asyncausführen, bei denen eine tatsächliche asynchrone Operation (normalerweise E / A) vorliegt, anstatt einen Thread zu blockieren, der sich befindet und auf ein Ergebnis wartet, kehrt der Thread zum Thread-Pool zurück und kann so eine andere Operation ausführen. Insgesamt bedeutet dies, dass Ihre Anwendung mit weniger Ressourcen mehr erreichen kann und die Skalierbarkeit verbessert.

i3arnon
quelle
3
@efaruk Alle Threads sind Worker-Threads. Das Freigeben eines ThreadPool-Threads und das Blockieren eines anderen Threads ist sinnlos.
i3arnon
1
@efaruk Ich bin mir nicht sicher, was Sie sagen wollen. Aber solange Sie zustimmen, gibt es keinen Grund, Async over Sync in WebAPI zu verwenden, ist es in Ordnung.
i3arnon
@efaruk "async over sync" (dh await Task.Run(() => CPUIntensive())) ist in asp.net nutzlos. Sie gewinnen nichts davon. Sie geben nur einen ThreadPool-Thread frei, um einen anderen zu belegen. Es ist weniger effizient als nur die synchrone Methode aufzurufen.
i3arnon
1
@efaruk Nein, das ist nicht vernünftig. In Ihrem Beispiel werden die unabhängigen Aufgaben nacheinander ausgeführt. Sie müssen sich unbedingt über asyc / await informieren, bevor Sie Empfehlungen abgeben können. Sie müssten verwenden await Task.WhenAll, um parallel auszuführen.
Søren Boisen
1
@efaruk Wie Boisen erklärt, bietet Ihr Beispiel keinen zusätzlichen Wert, wenn Sie diese synchronen Methoden einfach nacheinander aufrufen. Sie können verwenden, Task.Runwenn Sie Ihre Last auf mehreren Threads parallelisieren möchten, aber das bedeutet nicht "Async over Sync". "async over sync" verweist auf das Erstellen einer asynchronen Methode als Wrapper über eine synchrone. Sie können das im Zitat in meiner Antwort sehen.
i3arnon
1

Ein Ansatz könnte sein (ich habe dies erfolgreich in Kundenanwendungen verwendet), dass ein Windows-Dienst die langwierigen Vorgänge mit Arbeitsthreads ausführt und dies dann in IIS ausführt, um die Threads freizugeben, bis der Blockierungsvorgang abgeschlossen ist: Hinweis, dies setzt voraus Die Ergebnisse werden in einer Tabelle (durch jobId gekennzeichnete Zeilen) gespeichert und einige Stunden nach der Verwendung durch einen saubereren Prozess bereinigt.

Um die Frage zu beantworten: "Wie kann ich angesichts meines Problems und meines Kontexts davon profitieren, wenn die webAPI-Operation asynchron ausgeführt wird?" Da es sich um eine "ziemlich lange Operation" handelt, denke ich eher an viele Sekunden als an ms, werden durch diesen Ansatz IIS-Threads freigegeben. Natürlich müssen Sie auch einen Windows-Dienst ausführen, der selbst Ressourcen beansprucht, aber dieser Ansatz könnte verhindern, dass eine Flut langsamer Abfragen Threads aus anderen Teilen des Systems stiehlt.

// 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);
}

quelle