Wykonywać operacje asynchroniczne w ASP.NET MVC używa wątku z ThreadPool on.NET 4

Po tym pytaniu, sprawia mi to komfort podczas korzystania z asynchronicznego operacje w ASP.NET MVC. Tak więc napisałem dwa posty na blogu na ten temat:

Mam zbyt wiele nieporozumień w głowie co do operacji asynchronicznych on ASP.NET MVC.

Zawsze słyszę to zdanie: Aplikacja może skalować się lepiej, jeśli operacje są wykonywane asynchronicznie

I często słyszałem tego typu zdania: jeśli masz ogromny ruch, możesz lepiej nie wykonywać zapytań asynchronicznie - zużywanie 2 dodatkowych wątków do obsługi jednego żądania zabiera zasoby innym przychodzącym żądaniom.

Myślę, że te dwa zdania są niespójne.

Nie mam zbyt wielu informacji o tym, jak działa threadpool na ASP.NET ale wiem, że threadpool ma ograniczony rozmiar gwintów. Więc drugie zdanie musi być związane z tą kwestią.

I chciałbym wiedzieć, czy operacje asynchroniczne w ASP.NET MVC używa wątku z ThreadPool na. Net 4?

Na przykład, kiedy implementujemy AsyncController, jak działa struktura aplikacji? Jeśli dostaję ogromny ruch, czy jest to dobry pomysł, aby zaimplementować AsyncController?

Jest czy ktoś może zabrać mi tę czarną kurtynę przed oczyma i wyjaśnić mi sprawę z asynchronią na ASP.NET MVC 3 (NET 4)?

Edit:

Przeczytałem ten poniższy dokument prawie setki razy i Rozumiem główną sprawę, ale nadal mam zamieszanie, ponieważ jest zbyt wiele niespójnych komentarzy tam.

Korzystanie z kontrolera asynchronicznego w ASP.NET MVC

Edit:

Załóżmy, że mam kontroler akcja jak poniżej (choć nie implementacja AsyncController):

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

Jak widzisz, odpalam operację i o niej zapominam. Potem natychmiast wracam, nie czekając na zakończenie.

W tym przypadku, czy to musi używać wątku z threadpool? Jeśli tak, to co się stanie z tym wątkiem po jego zakończeniu? Czy GC przychodzi i sprzątać zaraz po zakończeniu?

Edit:

Dla odpowiedzi @Darin, oto próbka kodu asynchronicznego, który mówi do baza danych:

public class FooController : AsyncController {

    //EF 4.2 DbContext instance
    MyContext _context = new MyContext();

    public void IndexAsync() { 

        AsyncManager.OutstandingOperations.Increment(3);

        Task<IEnumerable<Foo>>.Factory.StartNew(() => { 

           return 
                _context.Foos;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foos"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<Bars>>.Factory.StartNew(() => { 

           return 
                _context.Bars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["bars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<FooBar>>.Factory.StartNew(() => { 

           return 
                _context.FooBars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foobars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });
    }

    public ViewResult IndexCompleted(
        IEnumerable<Foo> foos, 
        IEnumerable<Bar> bars,
        IEnumerable<FooBar> foobars) {

        //Do the regular stuff and return

    }
}
Author: tugberk, 2012-01-05

6 answers

Oto doskonały artykuł polecam lekturę, aby lepiej zrozumieć przetwarzanie asynchroniczne w ASP.NET (czyli to co w zasadzie reprezentują Kontrolery asynchroniczne).

Rozważmy najpierw standardowe działanie synchroniczne:

public ActionResult Index()
{
    // some processing
    return View();
}

W przypadku żądania tej akcji wątek jest pobierany z puli wątków, a treść tej akcji jest wykonywana w tym wątku. Więc jeśli przetwarzanie wewnątrz tej akcji jest powolne, blokujesz ten wątek dla całe przetwarzanie, więc ten wątek nie może być ponownie użyty do przetwarzania innych żądań. Po zakończeniu wykonywania żądania wątek jest zwracany do puli wątków.

Weźmy teraz przykład asynchronicznego wzoru:

public void IndexAsync()
{
    // perform some processing
}

public ActionResult IndexCompleted(object result)
{
    return View();
}

Gdy żądanie jest wysyłane do akcji Index, wątek jest pobierany z puli wątków i wykonywany jest korpus metody IndexAsync. Gdy ciało tej metody zakończy wykonywanie, wątek jest zwracany do puli wątków. Następnie, używając standardu AsyncManager.OutstandingOperations, gdy zasygnalizujesz zakończenie operacji asynchronicznej, z puli wątków jest pobierany kolejny wątek, na którym jest wykonywana akcja IndexCompleted, a wynik renderowany do klienta.

Więc w tym wzorze widzimy, że pojedyncze żądanie HTTP klienta może być wykonane przez dwa różne wątki.

Teraz interesująca część dzieje się wewnątrz metody IndexAsync. Jeśli masz w sobie operację blokującą, całkowicie marnujesz cały cel asynchronicznego Kontrolery, ponieważ blokujesz wątek roboczy (pamiętaj, że treść tej akcji jest wykonywana na wątku narysowanym z puli wątków).

Kiedy możemy naprawdę wykorzystać asynchroniczne Kontrolery, o które można zapytać?

IMHO możemy zyskać najwięcej, gdy mamy intensywne operacje We/Wy (takie jak bazy danych i połączenia sieciowe do usług zdalnych). Jeśli masz intensywną pracę procesora, akcje asynchroniczne nie przyniosą Ci większych korzyści.

Więc dlaczego możemy uzyskać korzyści z intensywnych operacji We / Wy? Ponieważ możemy użyć portów zakończenia wejścia / Wyjścia. IOCP są niezwykle wydajne, ponieważ nie zużywa się żadnych wątków ani zasobów na serwerze podczas wykonywania całej operacji.

Jak one działają?

Załóżmy, że chcemy pobrać zawartość zdalnej strony internetowej za pomocą elementu WebClient.DownloadStringAsync metoda. Wywołujesz tę metodę, która zarejestruje IOCP w systemie operacyjnym i natychmiast powróci. Podczas przetwarzania całego żądania na twoim serwerze nie są wykorzystywane żadne wątki. Wszystko dzieje się na zdalnym serwerze. To może zająć dużo czasu, ale nie obchodzi cię to, ponieważ nie narażasz swoich wątków pracowniczych. Po otrzymaniu odpowiedzi IOCP jest sygnalizowany, wątek jest pobierany z puli wątków i wywołanie zwrotne jest wykonywane w tym wątku. Ale jak widać, podczas całego procesu nie zmonopolizowaliśmy żadnych wątków.

To samo dotyczy metod takich jak FileStream.BeginRead, SqlCommand.BeginExecute,..

A co z równoległym wywołaniem wielu baz danych? Załóżmy, że miałeś synchroniczną akcję kontrolera, w której wykonałeś 4 blokowanie wywołań bazy danych w sekwencji. Łatwo obliczyć, że jeśli każde wywołanie bazy danych zajmuje 200ms, wykonanie działania kontrolera zajmie około 800ms.

Jeśli nie trzeba uruchamiać tych połączeń sekwencyjnie, czy równoległe ich poprawi wydajność?

To jest wielkie pytanie, na co nie jest łatwo odpowiedzieć. Może tak, może nie. Będzie to całkowicie zależeć od tego, jak zaimplementujesz te wywołania bazy danych. Jeśli używasz kontrolerów asynchronicznych i portów zakończenia We/Wy, jak omówiono wcześniej, zwiększysz wydajność tej akcji kontrolera i innych akcji, ponieważ nie będziesz monopolizować wątków roboczych.

Z drugiej strony, jeśli zaimplementujesz je źle (z blokującym wywołaniem bazy danych wykonanym na wątku z puli wątków), zasadniczo obniżysz sumę czas wykonania tej akcji do około 200ms, ale zużyłbyś 4 wątki robocze, więc możesz obniżyć wydajność innych żądań, które mogą stać się głodne z powodu brakujących wątków w puli, aby je przetworzyć.

Jest to więc bardzo trudne i jeśli nie czujesz się gotowy do przeprowadzenia obszernych testów swojej aplikacji, nie wdrażaj kontrolerów asynchronicznych, ponieważ są szanse, że zniesiesz więcej szkód niż korzyści. Wdrożyć je tylko wtedy, gdy masz powód, aby zrób to: na przykład stwierdziłeś, że standardowe działania sterownika synchronicznego są wąskim gardłem dla Twojej aplikacji(oczywiście po przeprowadzeniu obszernych testów obciążenia i pomiarów).

Rozważmy teraz twój przykład:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

Po otrzymaniu żądania dla akcji Index, wątek jest rysowany z puli wątków w celu wykonania jego ciała, ale jego ciało planuje tylko nowe zadanie za pomocą TPL. Tak więc wykonanie akcji kończy się i wątek jest zwracany do puli wątków. Oprócz tego, TPL używa wątków z puli wątków do ich przetwarzania. Więc nawet jeśli oryginalny wątek został zwrócony do puli wątków, narysowałeś inny wątek z tej puli, aby wykonać treść zadania. Więc naraziłeś 2 wątki ze swojej cennej puli.

Rozważmy teraz:

public ViewResult Index() { 

    new Thread(() => { 
        //Do an advanced looging here which takes a while
    }).Start();

    return View();
}

W tym przypadku ręcznie tworzymy wątek. W takim przypadku wykonanie akcji indeksu może potrwać nieco dłużej (bo zrobienie nowego wątku jest droższe niż wyciągnięcie go z istniejącej puli). Ale wykonanie zaawansowanej operacji logowania zostanie wykonane na wątku, który nie jest częścią puli. Nie narażamy więc wątków z Puli, które pozostają wolne do obsługi kolejnych próśb.

 167
Author: Darin Dimitrov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-28 06:46:42

Tak - wszystkie wątki pochodzą z puli wątków. Twoja aplikacja MVC jest już wielowątkowa, gdy pojawi się żądanie, nowy wątek zostanie pobrany z puli i użyty do obsługi żądania. Wątek ten będzie "zablokowany" (z innych żądań), dopóki żądanie nie zostanie w pełni obsłużone i zakończone. Jeśli nie ma wątku dostępnego w Puli, żądanie będzie musiało poczekać, aż jeden będzie dostępny.

Jeśli masz Kontrolery asynchroniczne nadal dostają wątek z puli, ale podczas serwisowania żądanie mogą zrezygnować z wątku, czekając, aż coś się wydarzy (i że wątek może być przekazany do innego żądania), a gdy pierwotne żądanie potrzebuje wątku ponownie, dostaje go z puli.

Różnica polega na tym, że jeśli masz wiele długotrwałych żądań (gdzie wątek czeka na odpowiedź od czegoś) , może zabraknąć wątków z puli do obsługi nawet podstawowych żądań. Jeśli masz Kontrolery asynchroniczne, nie masz więcej wątków, ale te wątki, które czekają, są zwracane do puli i mogą obsługiwać inne żądania.

A prawie prawdziwy przykład życia... Pomyśl o tym jak o wsiadaniu do autobusu, pięć osób czeka, aby wsiąść, pierwszy wsiada, płaci i siada (kierowca serwisował swoją prośbę), wsiadasz (kierowca serwisuje Twoją prośbę) , ale nie możesz znaleźć pieniędzy; jak grzebasz w kieszeniach, kierowca rezygnuje z Ciebie i dostaje następne dwie osoby (serwisujące swoje prośby), kiedy znajdziesz swoje pieniądze, nie możesz znaleźć pieniędzy. pieniądze kierowca znowu zaczyna się z Tobą zajmować (wypełniając Twoją prośbę) - piąta osoba musi poczekać, aż skończysz, ale trzecia i czwarta osoba dostali obsługę, gdy byłeś w połowie drogi do uzyskania obsługi. Oznacza to, że kierowca jest jedynym wątkiem z basenu, a pasażerowie są żądaniami. To było zbyt skomplikowane, aby napisać, jak to będzie działać, jeśli są dwa sterowniki, ale można sobie wyobrazić...

Bez kontrolera asynchronicznego pasażerowie za tobą musieliby poczekaj wieki, gdy szukasz pieniędzy, tymczasem kierowca autobusu nie robi żadnej pracy.

Wniosek jest taki, że jeśli Wiele osób nie wie, gdzie są ich pieniądze (tj. wymaga długiego czasu, aby odpowiedzieć na coś, o co poprosił kierowca) Kontrolery asynchroniczne mogą również pomóc w przepustowości żądań, przyspieszając proces od niektórych. Bez kontrolera aysnc każdy czeka, aż osoba z przodu zostanie całkowicie uporana. Ale nie zapominaj, że w MVC masz dużo autobusu sterowniki na jednej magistrali, więc asynchronizacja nie jest automatycznym wyborem.

 43
Author: K. Bob,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-05 14:12:32

W grę wchodzą dwie koncepcje. Przede wszystkim możemy sprawić, że nasz kod będzie działał równolegle, aby wykonać szybciej lub zaplanować kod w innym wątku, aby uniknąć oczekiwania użytkownika. Przykład, który miałeś

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

Należy do drugiej kategorii. Użytkownik otrzyma szybszą odpowiedź, ale całkowite obciążenie serwera jest wyższe, ponieważ musi wykonać tę samą pracę + obsłużyć wątek.

Innym przykładem może być:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Make async web request to twitter with WebClient.DownloadString()
    });

    Task.Factory.StartNew(() => { 
        //Make async web request to facebook with WebClient.DownloadString()
    });


    //wait for both to be ready and merge the results

    return View();
}

Ponieważ żądania działają równolegle użytkownika nie będzie musiał czekać tak długo, jak gdyby zrobili to seryjnie. Ale powinieneś zdawać sobie sprawę, że zużywamy więcej zasobów tutaj niż gdybyśmy działali seryjnie, ponieważ uruchamiamy kod w wielu wątkach, podczas gdy mamy na wątku czeka zbyt.

To jest w porządku w przypadku Klienta. I to jest dość powszechne, aby owinąć synchroniczny długo działający kod w nowym zadaniu (uruchom go w innym wątku) zbyt zachować responsywny interfejs lub paraleli, aby to szybciej. Wątek jest nadal używany przez cały czas trwania. Na serwerze z dużym obciążeniem może to spowodować awarię, ponieważ faktycznie używasz więcej zasobów. przed tym ostrzegali Cię ludzie

Kontrolery asynchroniczne w MVC mają jednak inny cel. Chodzi o to, aby uniknąć siedzeń wątków wokół nic nie robiących (co może zaszkodzić skalowalności). Tak naprawdę ma to znaczenie tylko wtedy, gdy wywołane przez Ciebie API mają metody asynchroniczne. Jak WebClient.DowloadStringAsync().

Chodzi o to, że możesz pozwolić, aby Twój wątek został zwrócony, aby obsłużyć nowy żądania do momentu zakończenia żądania internetowego, w którym zadzwoni do ciebie oddzwonienie, które otrzyma ten sam lub nowy wątek i zakończy żądanie.

Mam nadzieję, że rozumiesz różnicę między asynchronicznym a równoległym. Pomyśl o kodzie równoległym jako kodzie, w którym wątek siedzi i czeka na wynik. Podczas gdy kod asynchroniczny jest kodem, w którym zostaniesz powiadomiony o zakończeniu kodu i będziesz mógł wrócić do pracy nad nim, w międzyczasie wątek może wykonać inną pracę.
 9
Author: Mikael Eliasson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-05 14:54:46

Aplikacje Mogą skalować się lepiej, jeśli operacje są wykonywane asynchronicznie, ale tylko wtedy, gdy dostępne są zasoby do obsługi dodatkowych operacji .

Operacje asynchroniczne zapewniają, że nigdy nie blokujesz akcji, ponieważ istniejąca akcja jest w toku. ASP.NET posiada Model asynchroniczny, który umożliwia wykonywanie wielu żądań obok siebie. Byłoby możliwe kolejkowanie żądań i przetwarzanie ich FIFO, ale nie skalowałoby się to dobrze, gdy masz setki żądań w kolejce, a każde żądanie zajmuje 100ms do przetworzenia.

Jeśli masz duży ruch, Możesz lepiej nie wykonywać zapytań asynchronicznie, ponieważ może nie być dodatkowych zasobów do obsługi zapytań . Jeśli nie ma wolnych zasobów, Twoje żądania są zmuszone do kolejkowania się, trwają wykładniczo dłużej lub wręcz nie powiodą się, w takim przypadku asynchroniczne koszty ogólne (operacje muteksów i przełączania kontekstu) nie dają Ci cokolwiek.

As far as ASP.NET chodzi o to, że nie masz wyboru - używa modelu asynchronicznego, bo to ma sens dla modelu Serwer-Klient. Jeśli piszesz własny kod wewnętrznie, który używa wzorca asynchronicznego, aby próbować lepiej skalować, chyba że próbujesz zarządzać zasobem współdzielonym między wszystkimi żądaniami, nie zobaczysz żadnych ulepszeń, ponieważ są one już zawinięte w proces asynchroniczny, który niczego nie blokuje else.

Ostatecznie wszystko jest subiektywne, dopóki nie przyjrzysz się temu, co powoduje wąskie gardło w Twoim systemie. Czasami jest oczywiste, gdzie asynchroniczny wzorzec pomoże(zapobiegając blokowaniu zasobów w kolejce). Ostatecznie tylko Pomiar i analiza systemu może wskazać, gdzie można uzyskać wydajność.

Edit:

W twoim przykładzie, wywołanie Task.Factory.StartNew będzie ustawiać w kolejce operację w puli wątków. NET. Charakter wątków puli wątków jest do ponownego wykorzystania (do unikaj kosztów tworzenia / niszczenia wielu wątków). Po zakończeniu operacji wątek jest zwalniany z powrotem do puli w celu ponownego użycia przez inne żądanie(Garbage Collector w rzeczywistości nie angażuje się, chyba że utworzyłeś niektóre obiekty w swoich operacjach, w którym to przypadku są one zbierane zgodnie z normalnym zakresem).

As far as ASP.NET jest zaniepokojony, nie ma tu żadnej specjalnej operacji. Na ASP.NET żądanie kończy się bez względu na zadanie asynchroniczne. Jedynym problemem może być jeśli pula wątków jest nasycona (tzn. nie ma obecnie dostępnych wątków do obsługi żądania, a ustawienia puli nie pozwalają na utworzenie większej liczby wątków), w takim przypadku żądanie jest blokowane , czekając na rozpoczęcie zadania, dopóki wątek puli nie stanie się dostępny.

 6
Author: Paul Turner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-05 14:03:02

Tak, używają wątku z puli wątków. W rzeczywistości istnieje całkiem doskonały przewodnik z MSDN, który rozwiąże wszystkie Twoje pytania i nie tylko. W przeszłości było to bardzo przydatne. Zobacz!

Http://msdn.microsoft.com/en-us/library/ee728598.aspx

Tymczasem komentarze + sugestie, które usłyszysz o kodzie asynchronicznym, powinny być brane z przymrużeniem oka. Na początek, samo zrobienie czegoś asynchronicznego niekoniecznie powoduje, że jest skalowane lepiej, a w niektórych przypadkach może pogorszyć skalę aplikacji. Inny komentarz, który napisałeś o " ogromny ruch..."jest również poprawne tylko w pewnych kontekstach. To naprawdę zależy od tego, co robią twoje operacje i jak wchodzą w interakcję z innymi częściami systemu.

Krótko mówiąc, wiele osób ma wiele opinii na temat async, ale mogą one nie być poprawne z kontekstu. Powiedziałbym, że skup się na dokładnych problemach i wykonaj podstawowe testy wydajności, aby zobaczyć, jakie Kontrolery asynchroniczne, itd. właściwie obsłuż się Twoją aplikacją.

 2
Author: A.R.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-05 13:38:52

Pierwsza rzecz to nie MVC, ale IIS, który utrzymuje pulę wątków. Więc każde żądanie, które przychodzi do MVC lub ASP.NET aplikacja jest obsługiwana z wątków utrzymywanych w puli wątków. Dopiero przy tworzeniu aplikacji asynchronicznej wywołuje tę akcję w innym wątku i natychmiast zwalnia wątek, aby można było podjąć inne żądania.

To samo wyjaśniłem szczegółowym filmikiem ( http://www.youtube.com/watch?v=wvg13n5V0V0 / "kontrolery ASYNCH MVC i wątek starvation"), który pokazuje, jak thread starvation dzieje się w MVC i jak jego zminimalizowane przez użycie kontrolerów ASYNCH MVC.Zmierzyłem również kolejki żądań za pomocą perfmon, dzięki czemu możesz zobaczyć, jak kolejki żądań są zmniejszane dla ASYNCHINGU MVC i jak jest najgorzej dla operacji synchronizacji.

 0
Author: Shivprasad Koirala,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-29 12:21:58