Zawijanie kodu synchronicznego do wywołania asynchronicznego

97

Mam metodę w aplikacji ASP.NET, której wykonanie zajmuje dużo czasu. Wywołanie tej metody może wystąpić do 3 razy podczas jednego żądania użytkownika, w zależności od stanu pamięci podręcznej i parametrów podanych przez użytkownika. Każde połączenie trwa około 1-2 sekund. Sama metoda jest synchronicznym wywołaniem usługi i nie ma możliwości nadpisania implementacji.
Więc synchroniczne wywołanie usługi wygląda mniej więcej tak:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

A użycie metody to (zauważ, że wywołanie metody może się zdarzyć więcej niż raz):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

Próbowałem zmienić implementację z mojej strony, aby zapewnić jednoczesną pracę tej metody i oto do czego doszedłem do tej pory.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Użycie (część kodu „do innych rzeczy” działa jednocześnie z wywołaniem usługi):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Moje pytanie jest następujące. Czy używam właściwego podejścia, aby przyspieszyć wykonywanie w aplikacji ASP.NET, czy też wykonuję niepotrzebne zadanie, próbując uruchomić kod synchroniczny asynchronicznie? Czy ktoś może wyjaśnić, dlaczego drugie podejście nie jest opcją w ASP.NET (jeśli tak naprawdę nie jest)? Ponadto, jeśli takie podejście jest możliwe, czy muszę wywoływać taką metodę asynchronicznie, jeśli jest to jedyne wywołanie, jakie możemy w tej chwili wykonać (mam taki przypadek, gdy nie ma innych rzeczy do zrobienia w oczekiwaniu na zakończenie)?
Większość artykułów w sieci na ten temat dotyczy użycia async-awaitpodejścia z kodem, który już dostarcza awaitablemetod, ale to nie mój przypadek. Tutajto fajny artykuł opisujący mój przypadek, który nie opisuje sytuacji połączeń równoległych, rezygnując z opcji zawijania wywołania synchronizacji, ale moim zdaniem moja sytuacja jest właśnie do tego okazją.
Z góry dziękuję za pomoc i wskazówki.

Eadel
źródło

Odpowiedzi:

119

Ważne jest, aby rozróżnić dwa różne typy współbieżności. Współbieżność asynchroniczna występuje wtedy, gdy w locie jest wiele operacji asynchronicznych (a ponieważ każda operacja jest asynchroniczna, żadna z nich nie używa w rzeczywistości wątku ). Współbieżność równoległa występuje wtedy, gdy masz wiele wątków, z których każdy wykonuje oddzielną operację.

Pierwszą rzeczą do zrobienia jest ponowna ocena tego założenia:

Sama metoda jest synchronicznym wywołaniem usługi i nie ma możliwości nadpisania implementacji.

Jeśli Twoja „usługa” jest usługą internetową lub czymkolwiek innym, co jest związane z wejściem / wyjściem, najlepszym rozwiązaniem jest napisanie dla niej asynchronicznego interfejsu API.

Będę kontynuował założenie, że twoja „usługa” to operacja związana z CPU, która musi być wykonywana na tej samej maszynie co serwer WWW.

Jeśli tak jest, następną rzeczą do oceny jest inne założenie:

Potrzebuję szybszego wykonania żądania.

Czy jesteś absolutnie pewien, że to właśnie musisz zrobić? Czy są jakieś zmiany w interfejsie użytkownika, które możesz zamiast tego wprowadzić - np. Uruchomić żądanie i pozwolić użytkownikowi na wykonanie innej pracy podczas jego przetwarzania?

Będę wychodzić z założenia, że ​​tak, naprawdę musisz sprawić, by indywidualne żądanie było wykonywane szybciej.

W takim przypadku musisz wykonać równoległy kod na swoim serwerze internetowym. Zdecydowanie nie jest to ogólnie zalecane, ponieważ kod równoległy będzie korzystał z wątków, których ASP.NET może potrzebować do obsługi innych żądań, a usunięcie / dodanie wątków spowoduje wyłączenie heurystyki puli wątków ASP.NET. Tak więc ta decyzja ma wpływ na cały serwer.

Korzystając z kodu równoległego w ASP.NET, podejmujesz decyzję o rzeczywistym ograniczeniu skalowalności swojej aplikacji internetowej. Możesz również zauważyć spore zmiany w wątku, zwłaszcza jeśli twoje żądania są w ogóle pęknięte. Zalecam używanie kodu równoległego na ASP.NET tylko wtedy, gdy wiesz, że liczba jednoczesnych użytkowników będzie dość mała (tj. Nie jest to serwer publiczny).

Tak więc, jeśli zajdziesz tak daleko i jesteś pewien, że chcesz wykonać przetwarzanie równoległe w ASP.NET, masz kilka opcji.

Jedną z łatwiejszych metod jest użycie Task.Runbardzo podobnej do istniejącego kodu. Jednak nie polecam implementowania CalculateAsyncmetody, ponieważ oznacza to, że przetwarzanie jest asynchroniczne (a tak nie jest). Zamiast tego użyj Task.Runw momencie połączenia:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Ewentualnie, jeśli to działa dobrze z kodem, można użyć Paralleltypu, czyli Parallel.For, Parallel.ForEachalbo Parallel.Invoke. Zaletą Parallelkodu jest to, że wątek żądania jest używany jako jeden z równoległych wątków, a następnie wznawia wykonywanie w kontekście wątku (jest mniej przełączania kontekstu niż w asyncprzykładzie):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

W ogóle nie polecam używania Parallel LINQ (PLINQ) w ASP.NET.

Stephen Cleary
źródło
1
Bardzo dziękuję za szczegółową odpowiedź. Jeszcze jedna rzecz, którą chcę wyjaśnić. Kiedy zaczynam wykonywanie przy użyciu Task.Run, zawartość jest uruchamiana w innym wątku, który jest pobierany z puli wątków. Wtedy nie ma potrzeby zawijania wywołań blokujących (takich jak moje wywołanie usługi) w Runs, ponieważ każdy z nich będzie zawsze zużywał jeden wątek, który zostanie zablokowany podczas wykonywania metody. W takiej sytuacji jedyną korzyścią, jaka async-awaitmi pozostała, jest wykonywanie kilku czynności na raz. Proszę popraw mnie jeżeli się mylę.
Eadel
2
Tak, jedyną korzyścią, jaką uzyskujesz z Run(lub Parallel), jest współbieżność. Każda operacja nadal blokuje wątek. Ponieważ twierdzisz, że usługa jest usługą sieciową, nie polecam używania Runlub Parallel; Zamiast tego napisz asynchroniczny interfejs API dla usługi.
Stephen Cleary
3
@StephenCleary. co masz na myśli przez „… a ponieważ każda operacja jest asynchroniczna, żadna z nich nie używa w rzeczywistości wątku…” dzięki!
alltej
3
@alltej: W przypadku prawdziwie asynchronicznego kodu nie ma wątku .
Stephen Cleary
4
@JoshuaFrank: Nie bezpośrednio. Kod za synchronicznym interfejsem API musi z definicji blokować wątek wywołujący. Państwo może używać asynchronicznego API jak BeginGetResponsei rozpocząć kilka z nich, a następnie (synchronicznie) blok aż wszystkie z nich są kompletne, ale takie podejście jest trudne - zobacz blog.stephencleary.com/2012/07/dont-block-on -async-code.html i msdn.microsoft.com/en-us/magazine/mt238404.aspx . Jeśli to możliwe, zwykle łatwiej i czystsze jest przyjęcie asynccałej metody.
Stephen Cleary
1

Zauważyłem, że poniższy kod może przekonwertować Task, aby zawsze działał asynchronicznie

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

i użyłem go w następujący sposób

await ForceAsync(() => AsyncTaskWithNoAwaits())

Spowoduje to asynchroniczne wykonanie dowolnego zadania, dzięki czemu można je połączyć w scenariuszach WhenAll, WhenAny i innych zastosowaniach.

Możesz także po prostu dodać Task.Yield () jako pierwszą linię wywoływanego kodu.

kernowcode
źródło