Verwenden Sie async / await effektiv mit der ASP.NET-Web-API

114

Ich versuche, die async/awaitFunktion von ASP.NET in meinem Web-API-Projekt zu verwenden. Ich bin mir nicht sicher, ob sich dies auf die Leistung meines Web-API-Dienstes auswirkt. Nachfolgend finden Sie den Workflow und den Beispielcode aus meiner Bewerbung.

Arbeitsablauf:

UI-Anwendung → Web-API-Endpunkt (Controller) → Aufrufmethode in der Web-API-Serviceschicht → Rufen Sie einen anderen externen Webdienst auf. (Hier haben wir die DB-Interaktionen usw.)

Regler:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

Serviceschicht:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

Ich habe den obigen Code getestet und funktioniert. Ich bin mir aber nicht sicher, ob es die richtige Verwendung von ist async/await. Bitte teilen Sie Ihre Gedanken.

arp
quelle

Antworten:

201

Ich bin mir nicht sicher, ob sich dies auf die Leistung meiner API auswirkt.

Beachten Sie, dass der Hauptvorteil von asynchronem Code auf der Serverseite die Skalierbarkeit ist . Es wird Ihre Anfragen nicht auf magische Weise beschleunigen. asyncIn meinem Artikel über asyncASP.NET gehe ich auf einige Überlegungen ein, die ich verwenden sollte .

Ich denke, Ihr Anwendungsfall (Aufruf anderer APIs) eignet sich gut für asynchronen Code. Denken Sie jedoch daran, dass "asynchron" nicht "schneller" bedeutet. Der beste Ansatz besteht darin, Ihre Benutzeroberfläche zunächst reaktionsschnell und asynchron zu gestalten. Dadurch fühlt sich Ihre App schneller an, auch wenn sie etwas langsamer ist.

Was den Code betrifft, ist dies nicht asynchron:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

Sie benötigen eine wirklich asynchrone Implementierung, um die Skalierbarkeitsvorteile von async:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Oder (wenn Ihre Logik in dieser Methode wirklich nur ein Durchgang ist):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Beachten Sie, dass es einfacher ist, von innen nach außen zu arbeiten, als von außen nach innen. Mit anderen Worten, beginnen Sie nicht mit einer asynchronen Controller-Aktion und erzwingen Sie dann, dass nachgeschaltete Methoden asynchron sind. Identifizieren Sie stattdessen die natürlich asynchronen Vorgänge (Aufrufen externer APIs, Datenbankabfragen usw.) und machen Sie diese zuerst auf der untersten Ebene asynchron ( Service.ProcessAsync). Lassen Sie dann das asyncRinnsal hoch und machen Sie Ihre Controller-Aktionen als letzten Schritt asynchron.

Und unter keinen Umständen sollten Sie Task.Runin diesem Szenario verwenden.

Stephen Cleary
quelle
4
Vielen Dank an Stephen für Ihre wertvollen Kommentare. Ich habe alle meine Service-Layer-Methoden so geändert, dass sie asynchron sind. Jetzt rufe ich meinen externen REST-Aufruf mit der ExecuteTaskAsync-Methode auf und arbeite wie erwartet. Vielen Dank auch für Ihre Blog-Beiträge zu Async und Aufgaben. Es hat mir sehr geholfen, ein erstes Verständnis zu bekommen.
Arp
5
Können Sie erklären, warum Sie Task.Run hier nicht verwenden sollten?
Maarten
1
@ Maarten: Ich erkläre es (kurz) in dem Artikel, auf den ich verlinkt habe . Ich gehe in meinem Blog näher darauf ein .
Stephen Cleary
Ich habe Ihren Artikel Stephen gelesen und es scheint zu vermeiden, etwas zu erwähnen. Wenn eine ASP.NET-Anforderung eintrifft und beginnt, wird sie in einem Thread-Pool-Thread ausgeführt, ist das in Ordnung. Wenn es jedoch asynchron wird, initiiert es die asynchrone Verarbeitung und dieser Thread kehrt sofort zum Pool zurück. Die in der asynchronen Methode ausgeführte Arbeit wird jedoch selbst in einem Thread-Pool-Thread ausgeführt!
Hugh
12

Es ist richtig, aber vielleicht nicht nützlich.

Da es nichts zu warten gibt - keine Aufrufe zum Blockieren von APIs, die asynchron arbeiten könnten -, richten Sie Strukturen ein, um den asynchronen Betrieb (mit Overhead) zu verfolgen, nutzen diese Funktion jedoch nicht.

Wenn die Serviceschicht beispielsweise DB-Vorgänge mit Entity Framework ausführt, das asynchrone Aufrufe unterstützt:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

Sie würden dem Worker-Thread erlauben, etwas anderes zu tun, während die Datenbank abgefragt wurde (und somit eine andere Anforderung verarbeiten können).

Warten ist in der Regel etwas, das den ganzen Weg nach unten gehen muss: Es ist sehr schwierig, es in ein bestehendes System nachzurüsten.

Richard
quelle
-1

Sie nutzen async / await nicht effektiv, da der Anforderungsthread beim Ausführen der synchronen Methode blockiert wirdReturnAllCountries()

Der Thread, der für die Bearbeitung einer Anfrage zugewiesen ist, wartet während ReturnAllCountries()der Arbeit im Leerlauf .

Wenn Sie ReturnAllCountries()asynchron implementieren können, sehen Sie Vorteile für die Skalierbarkeit. Dies liegt daran, dass der Thread möglicherweise wieder in den .NET-Thread-Pool freigegeben wird, um eine andere Anforderung zu verarbeiten, während er ReturnAllCountries()ausgeführt wird. Dies würde Ihrem Dienst einen höheren Durchsatz ermöglichen, indem Threads effizienter genutzt werden.

James Wierzba
quelle
Das ist nicht wahr. Der Socket ist verbunden, aber der Thread, der die Anforderung verarbeitet, kann etwas anderes tun, z. B. eine andere Anforderung verarbeiten, während er auf einen Serviceabruf wartet.
Garr Godfrey
-8

Ich würde Ihre Serviceschicht ändern in:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

So wie Sie es haben, führen Sie Ihren _service.ProcessAnruf immer noch synchron aus und profitieren nur sehr wenig oder gar nicht davon, darauf zu warten.

Mit diesem Ansatz verpacken Sie den potenziell langsamen Anruf in a Task, starten ihn und geben ihn zurück, damit er erwartet wird. Jetzt haben Sie den Vorteil, auf das zu warten Task.

Jonesopolis
quelle
3
Laut Blog- Link konnte ich sehen, dass die Methode auch dann synchron ausgeführt wird, wenn wir Task.Run verwenden.
Arp
Hmm. Es gibt viele Unterschiede zwischen dem obigen Code und diesem Link. Your Serviceist keine asynchrone Methode und wird im ServiceAufruf selbst nicht erwartet . Ich kann seinen Standpunkt verstehen, aber ich glaube nicht, dass er hier zutrifft.
Jonesopolis
8
Task.Runsollte unter ASP.NET vermieden werden. Dieser Ansatz würde alle Vorteile von async/ entfernen awaitund unter Last eine schlechtere Leistung erzielen als nur ein synchroner Anruf.
Stephen Cleary