Dlaczego powinienem tworzyć asynchroniczne operacje WebAPI zamiast synchronizować operacje?

109

Mam następującą operację w utworzonym przeze mnie interfejsie API sieci Web:

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

Wywołanie tej usługi sieciowej odbywa się za pośrednictwem wywołania Jquery Ajax w następujący sposób:

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

Widziałem kilku programistów, którzy implementują poprzednią operację w ten sposób:

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

Muszę jednak powiedzieć, że GetProductsWithHistory () to dość długa operacja. Biorąc pod uwagę mój problem i kontekst, jakie korzyści przyniesie mi asynchroniczna operacja interfejsu webAPI?

David Jiménez Martínez
źródło
1
Po stronie klienta używa AJAX, który jest już asynchroniczny. Nie ma potrzeby, aby usługa była również zapisywana jako plik async Task<T>. Pamiętaj, AJAX został zaimplementowany zanim jeszcze istniał TPL :)
Dominic Zukiewicz
65
Musisz zrozumieć, dlaczego wdrażasz kontrolery asynchroniczne, a wielu tego nie robi. Usługi IIS mają ograniczoną liczbę dostępnych wątków, a gdy wszystkie są w użyciu, serwer nie może przetwarzać nowych żądań. W przypadku kontrolerów asynchronicznych, gdy proces oczekuje na zakończenie operacji we / wy, jego wątek jest zwalniany, aby serwer mógł używać go do przetwarzania innych żądań.
Matija Grcic
3
Którzy programiści to robili? Jeśli na blogu jest jakiś post lub artykuł zalecający tę technikę, zamieść link.
Stephen Cleary
3
Pełną korzyść z asynchronizacji można uzyskać tylko wtedy, gdy proces jest świadomy asynchronizacji od góry (w tym samej aplikacji internetowej i kontrolerów) do wszelkich oczekujących działań wychodzących poza proces (w tym opóźnienia czasomierza, we / wy pliku, dostęp do bazy danych, i żądań internetowych). W takim przypadku pomocnik delegata wymaga GetProductsWithHistoryAsync()zwrotu Task<CartTotalsDTO>. Zapisanie kontrolera w trybie asynchronicznym może przynieść korzyści, jeśli zamierzasz migrować wywołania, które wywołuje, aby były również asynchroniczne; następnie zaczniesz czerpać korzyści z części asynchronicznych podczas migracji pozostałych.
Keith Robertson
1
Jeśli proces, który wykonujesz, kończy się i trafia do bazy danych, Twój wątek sieciowy po prostu czeka, aż wróci i zatrzyma ten wątek. Jeśli osiągnąłeś maksymalną liczbę wątków i pojawi się kolejne żądanie, musi poczekać. Dlaczego to robisz? Zamiast tego chcesz zwolnić ten wątek ze swojego kontrolera, aby inne żądanie mogło go użyć i zająć inny wątek sieci Web tylko wtedy, gdy powróci pierwotne żądanie z bazy danych. msdn.microsoft.com/en-us/magazine/dn802603.aspx
user441521

Odpowiedzi:

98

W twoim konkretnym przykładzie operacja nie jest w ogóle asynchroniczna, więc to, co robisz, to asynchronizacja przez synchronizację. Po prostu zwalniasz jeden wątek i blokujesz inny. Nie ma powodu, ponieważ wszystkie wątki są wątkami puli wątków (w przeciwieństwie do aplikacji GUI).

W mojej dyskusji o „async over sync” zdecydowanie zasugerowałem, że jeśli masz interfejs API, który wewnętrznie jest implementowany synchronicznie, nie powinieneś ujawniać asynchronicznego odpowiednika, który po prostu otacza metodę synchroniczną Task.Run.

Od Czy należy uwidaczniać otoki synchroniczne dla metod asynchronicznych?

Jednak podczas wykonywania wywołań asyncinterfejsu WebAPI, w których występuje rzeczywista operacja asynchroniczna (zwykle we / wy) zamiast blokowania wątku, który siedzi i czeka na wynik, wątek wraca do puli wątków, dzięki czemu może wykonać inną operację. Oznacza to, że Twoja aplikacja może zrobić więcej przy mniejszych zasobach, a to poprawia skalowalność.

i3arnon
źródło
3
@efaruk wszystkie wątki są wątkami roboczymi. Zwalnianie jednego wątku ThreadPool i blokowanie innego jest bezcelowe.
i3arnon
1
@efaruk Nie jestem pewien, co próbujesz powiedzieć ... ale jeśli się zgadzasz, nie ma powodu, aby używać async przez synchronizację w WebAPI, to jest w porządku.
i3arnon
@efaruk „async over sync” (tj. await Task.Run(() => CPUIntensive())) jest bezużyteczne w asp.net. Nic na tym nie zyskujesz. Po prostu zwalniasz jeden wątek ThreadPool, aby zająć inny. Jest to mniej wydajne niż zwykłe wywołanie metody synchronicznej.
i3arnon
1
@efaruk Nie, to nie jest rozsądne. Na przykład uruchamiasz niezależne zadania sekwencyjnie. Naprawdę musisz przeczytać asyc / await przed wydaniem rekomendacji. Musisz użyć await Task.WhenAll, aby wykonywać równolegle.
Søren Boisen
1
@efaruk Jak wyjaśnia Boisen, Twój przykład nie dodaje żadnej wartości poza prostym wywoływaniem tych metod synchronicznych po kolei. Możesz użyć, Task.Runjeśli chcesz zrównoleglenie obciążenia w wielu wątkach, ale nie to oznacza „asynchronizacja przez synchronizację”. „async over sync” odwołuje się do tworzenia metody asynchronicznej jako otoki względem metody synchronicznej. Możesz zobaczyć w cytacie w mojej odpowiedzi.
i3arnon
1

Jednym podejściem mogłoby być (z powodzeniem stosowałem to w aplikacjach klientów), aby usługa Windows uruchamiała długie operacje z wątkami roboczymi, a następnie robiła to w usługach IIS, aby zwolnić wątki do momentu zakończenia operacji blokowania: Uwaga, zakłada się, że wyniki są przechowywane w tabeli (wiersze identyfikowane przez jobId), a czystszy proces czyści je kilka godzin po użyciu.

Odpowiadając na pytanie: „Biorąc pod uwagę mój problem i kontekst, jakie korzyści przyniesie mi ustawienie asynchronicznej operacji interfejsu webAPI?” biorąc pod uwagę, że jest to „dość długa operacja”, myślę o wielu sekundach, a nie ms, takie podejście zwalnia wątki IIS. Oczywiście musisz również uruchomić usługę Windows, która sama pobiera zasoby, ale takie podejście może zapobiec zalewowi powolnych zapytań przed kradzieżą wątków z innych części systemu.

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

źródło