Efektywnie używaj async / await z ASP.NET Web API

114

Próbuję skorzystać z async/awaitfunkcji ASP.NET w moim projekcie interfejsu API sieci Web. Nie jestem pewien, czy wpłynie to na wydajność mojej usługi Web API. Poniżej przedstawiam przebieg pracy i przykładowy kod z mojej aplikacji.

Przepływ pracy:

Aplikacja UI → Punkt końcowy Web API (kontroler) → Metoda wywołania w warstwie usług Web API → Wywołaj inną zewnętrzną usługę internetową. (Tutaj mamy interakcje bazy danych itp.)

Kontroler:

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

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

    return InternalServerError();
}

Warstwa usług:

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

    return Task.FromResult(response);
}

Przetestowałem powyższy kod i działa. Ale nie jestem pewien, czy jest to właściwe użycie async/await. Podziel się swoimi przemyśleniami.

arp
źródło

Odpowiedzi:

203

Nie jestem pewien, czy wpłynie to na wydajność mojego API.

Należy pamiętać, że podstawową zaletą kodu asynchronicznego po stronie serwera jest skalowalność . W magiczny sposób nie sprawi, że Twoje żądania będą działać szybciej. asyncW moim artykule na temat ASP.NET omówię kilka kwestii, które należy wziąć pod uwagęasync .

Myślę, że Twój przypadek użycia (wywoływanie innych interfejsów API) jest odpowiedni dla kodu asynchronicznego, pamiętaj tylko, że „asynchroniczny” nie oznacza „szybszy”. Najlepszym podejściem jest ustawienie najpierw responsywnego i asynchronicznego interfejsu użytkownika ; dzięki temu Twoja aplikacja będzie działać szybciej, nawet jeśli jest to nieco wolniej.

Jeśli chodzi o kod, nie jest to asynchroniczne:

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

Potrzebujesz prawdziwie asynchronicznej implementacji, aby uzyskać korzyści związane ze skalowalnością async:

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

Lub (jeśli twoja logika w tej metodzie naprawdę jest tylko przejściem):

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

Zwróć uwagę, że łatwiej jest pracować „od wewnątrz” niż „od zewnątrz do wewnątrz” w ten sposób. Innymi słowy, nie rozpoczynaj od asynchronicznej akcji kontrolera, a następnie wymuszaj asynchroniczne metody podrzędne. Zamiast tego zidentyfikuj naturalnie asynchroniczne operacje (wywoływanie zewnętrznych interfejsów API, zapytania do bazy danych itp.) I ustaw je jako asynchroniczne najpierw na najniższym poziomie ( Service.ProcessAsync). Następnie pozwól, aby asyncściekała, sprawiając, że akcje kontrolera będą asynchroniczne jako ostatni krok.

I w żadnym wypadku nie powinieneś używać Task.Runw tym scenariuszu.

Stephen Cleary
źródło
4
Dzięki Stephen za cenne uwagi. Zmieniłem wszystkie metody warstwy usług na asynchroniczne, a teraz wywołuję moje zewnętrzne wywołanie REST za pomocą metody ExecuteTaskAsync i działa zgodnie z oczekiwaniami. Dziękuję również za posty na blogu dotyczące asynchronii i zadań. To naprawdę bardzo mi pomogło, aby uzyskać wstępne zrozumienie.
arp
5
Czy mógłbyś wyjaśnić, dlaczego nie powinieneś używać Task.Run tutaj?
Maarten
1
@Maarten: Wyjaśniam to (krótko) w artykule, do którego mam link . Bardziej szczegółowo opisuję na moim blogu .
Stephen Cleary
Przeczytałem twój artykuł Stephen i wydaje się, że unikam wzmianki o czymś. Gdy przychodzi i rozpoczyna żądanie ASP.NET, działa w wątku puli wątków, nie ma problemu. Ale jeśli stanie się asynchroniczny, to tak, inicjuje przetwarzanie asynchroniczne i ten wątek natychmiast wraca do puli. Ale praca wykonana w metodzie asynchronicznej sama zostanie wykonana w wątku puli wątków!
Hugh
12

Jest poprawny, ale być może nieprzydatny.

Ponieważ nie ma na co czekać - nie ma wywołań blokujących interfejsów API, które mogłyby działać asynchronicznie - wtedy konfigurujesz struktury do śledzenia operacji asynchronicznych (które mają narzut), ale nie korzystasz z tej możliwości.

Na przykład, jeśli warstwa usług wykonywała operacje DB z Entity Framework, która obsługuje wywołania asynchroniczne:

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

       return list;
    }
}

Pozwoliłbyś wątkowi robocemu na zrobienie czegoś innego, podczas gdy baza danych była odpytywana (a tym samym mogła przetworzyć inne żądanie).

Oczekiwanie wydaje się być czymś, co musi spaść do końca: bardzo trudno jest dopasować go do istniejącego systemu.

Richard
źródło
-1

Nie wykorzystujesz efektywnie async / await, ponieważ wątek żądania zostanie zablokowany podczas wykonywania metody synchronicznejReturnAllCountries()

Wątek, który jest przypisany do obsługi żądania, będzie bezczynnie czekał, ReturnAllCountries()aż będzie działać.

Jeśli możesz zaimplementować ReturnAllCountries()w sposób asynchroniczny, zobaczysz korzyści związane ze skalowalnością. Dzieje się tak, ponieważ wątek może zostać zwolniony z powrotem do puli wątków platformy .NET w celu obsługi innego żądania podczas ReturnAllCountries()wykonywania. Pozwoliłoby to Twojej usłudze mieć wyższą przepustowość dzięki wydajniejszemu wykorzystaniu wątków.

James Wierzba
źródło
To nie jest prawda. Gniazdo jest połączone, ale wątek przetwarzający żądanie może zrobić coś innego, na przykład przetworzyć inne żądanie, podczas oczekiwania na wezwanie usługi.
Garr Godfrey
-8

Zmieniłbym Twoją warstwę usług na:

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

tak jak masz, nadal prowadzisz _service.Processpołączenie synchronicznie, a oczekiwanie na nie przynosi znikome korzyści lub wcale.

Dzięki takiemu podejściu zawijasz potencjalnie wolne połączenie w a Task, uruchamiasz je i zwracasz na oczekiwanie. Teraz możesz oczekiwać na Task.

Jonesopolis
źródło
3
Zgodnie z linkiem do bloga mogłem zobaczyć, że nawet jeśli używamy Task.Run, metoda nadal działa synchronicznie?
arp
Hmm. Istnieje wiele różnic między powyższym kodem a tym łączem. Your Servicenie jest metodą asynchroniczną i nie jest oczekiwana w samym Servicewywołaniu. Rozumiem jego punkt widzenia, ale nie wierzę, że ma to zastosowanie tutaj.
Jonesopolis
8
Task.Runnależy unikać w ASP.NET. Takie podejście usuwałoby wszystkie korzyści z async/ awaiti faktycznie działało gorzej pod obciążeniem niż tylko wywołanie synchroniczne.
Stephen Cleary