Po tym pytaniu czuję się komfortowo podczas korzystania z operacji asynchronicznych w ASP.NET MVC. Napisałem więc dwa posty na blogu na ten temat:
Mam w głowie zbyt wiele nieporozumień dotyczących operacji asynchronicznych w ASP.NET MVC.
Zawsze słyszę to zdanie: aplikacja może się lepiej skalować, jeśli operacje są wykonywane asynchronicznie
Często też słyszałem tego rodzaju zdania: jeśli masz duży ruch, lepiej nie wykonywać zapytań asynchronicznie - zużywanie 2 dodatkowych wątków do obsługi jednego żądania zabiera zasoby innym żądaniom przychodzącym.
Myślę, że te dwa zdania są niespójne.
Nie mam zbyt wielu informacji o tym, jak działa Threadpool w ASP.NET, ale wiem, że Threadpool ma ograniczony rozmiar dla wątków. Zatem drugie zdanie musi odnosić się do tej kwestii.
I chciałbym wiedzieć, czy operacje asynchroniczne w ASP.NET MVC używają wątku z ThreadPool na .NET 4?
Na przykład, kiedy implementujemy AsyncController, w jaki sposób struktury aplikacji? Jeśli otrzymuję duży ruch, czy warto zaimplementować AsyncController?
Czy jest ktoś, kto może zdjąć tę czarną zasłonę na moich oczach i wyjaśnić mi ofertę asynchronii w ASP.NET MVC 3 (NET 4)?
Edytować:
Przeczytałem poniższy dokument prawie setki razy i rozumiem główną sprawę, ale nadal mam zamieszanie, ponieważ jest tam zbyt wiele niespójnych komentarzy.
Korzystanie z kontrolera asynchronicznego w ASP.NET MVC
Edytować:
Załóżmy, że mam akcję kontrolera jak poniżej (nie jest to jednak implementacja AsyncController
):
public ViewResult Index() {
Task.Factory.StartNew(() => {
//Do an advanced looging here which takes a while
});
return View();
}
Jak widzisz, odpalam operację i zapominam o niej. Następnie wracam natychmiast, nie czekając na zakończenie.
W tym przypadku, czy to musi używać wątku z puli wątków? Jeśli tak, po zakończeniu, co się stanie z tym wątkiem? Czy GC
pojawia się i czyści zaraz po zakończeniu?
Edytować:
Aby uzyskać odpowiedź @ Darin, oto przykład kodu asynchronicznego, który komunikuje się z bazą 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
}
}
źródło
Odpowiedzi:
Oto doskonały artykuł , który polecam przeczytać, aby lepiej zrozumieć przetwarzanie asynchroniczne w ASP.NET (co zasadniczo reprezentują kontrolery asynchroniczne).
Rozważmy najpierw standardową akcję synchroniczną:
Gdy żądanie jest skierowane do tej akcji, z puli wątków jest rysowany wątek, a treść tej akcji jest wykonywana w tym wątku. Więc jeśli przetwarzanie w ramach tej akcji jest powolne, blokujesz ten wątek dla całego przetwarzania, więc ten wątek nie może być ponownie użyty do przetwarzania innych żądań. Pod koniec wykonywania żądania wątek jest zwracany do puli wątków.
Teraz weźmy przykład wzorca asynchronicznego:
Gdy żądanie jest wysyłane do akcji Index, wątek jest rysowany z puli wątków i
IndexAsync
wykonywana jest treść metody. Po zakończeniu wykonywania treści tej metody wątek jest zwracany do puli wątków. Następnie, używając standarduAsyncManager.OutstandingOperations
, po zasygnalizowaniu zakończenia operacji asynchronicznej, z puli wątków jest rysowany kolejny wątek iIndexCompleted
wykonywana jest na nim treść akcji, a wynik renderowany klientowi.Widzimy więc w tym wzorcu, że pojedyncze żądanie HTTP klienta może zostać wykonane przez dwa różne wątki.
Teraz interesująca część dzieje się wewnątrz
IndexAsync
metody. Jeśli masz w sobie operację blokującą, całkowicie marnujesz cały cel kontrolerów asynchronicznych, ponieważ blokujesz wątek roboczy (pamiętaj, że treść tej akcji jest wykonywana na wątku narysowanym z puli wątków).Kiedy więc możemy naprawdę skorzystać z kontrolerów asynchronicznych, o które możesz zapytać?
IMHO możemy zyskać najwięcej, gdy mamy intensywne operacje we / wy (takie jak bazy danych i wywołania sieciowe do usług zdalnych). Jeśli masz operację intensywnie wykorzystującą procesor, akcje asynchroniczne nie przyniosą wielu korzyści.
Dlaczego więc możemy czerpać korzyści z intensywnych operacji we / wy? Ponieważ moglibyśmy użyć portów zakończenia we / wy . IOCP są niezwykle potężne, ponieważ nie zużywasz żadnych wątków ani zasobów na serwerze podczas wykonywania całej operacji.
Jak oni pracują?
Załóżmy, że chcemy pobrać zawartość zdalnej strony internetowej przy użyciu metody WebClient.DownloadStringAsync . Wywołujesz tę metodę, która zarejestruje IOCP w systemie operacyjnym i natychmiast powróci. Podczas przetwarzania całego żądania na serwerze nie są używane żadne wątki. Wszystko dzieje się na zdalnym serwerze. Może to zająć dużo czasu, ale nie przejmujesz się, ponieważ nie narażasz swoich wątków roboczych. Po odebraniu odpowiedzi sygnalizowany jest IOCP, z puli wątków pobierany jest wątek i w tym wątku wykonywane jest wywołanie zwrotne. 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łością wielu wywołań bazy danych? Załóżmy, że masz synchroniczną akcję kontrolera, w której wykonałeś kolejno 4 blokujące wywołania bazy danych. Łatwo obliczyć, że jeśli każde wywołanie bazy danych trwa 200 ms, wykonanie akcji kontrolera zajmie około 800 ms.
Jeśli nie musisz uruchamiać tych wywołań po kolei, czy zrównoleglenie ich poprawi wydajność?
To jest wielkie pytanie, na które nie jest łatwo odpowiedzieć. Może tak może nie. Będzie to całkowicie zależało 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, a także innych akcji, ponieważ nie będziesz monopolizować wątków roboczych.
Z drugiej strony, jeśli zaimplementujesz je słabo (z blokującym wywołaniem bazy danych wykonywanym na wątku z puli wątków), zasadniczo zmniejszysz całkowity czas wykonywania tej akcji do około 200 ms, ale zużywałbyś 4 wątki robocze, więc mogło obniżyć wydajność innych żądań, które mogą stać się głodne z powodu brakujących wątków w puli do ich przetwarzania.
Jest to więc bardzo trudne i jeśli nie czujesz się gotowy do wykonywania obszernych testów swojej aplikacji, nie wdrażaj kontrolerów asynchronicznych, ponieważ istnieje szansa, że wyrządzisz więcej szkód niż korzyści. Wdrażaj je tylko wtedy, gdy masz ku temu powód: na przykład zidentyfikowałeś, że standardowe akcje kontrolera synchronicznego są wąskim gardłem dla twojej aplikacji (oczywiście po wykonaniu obszernych testów obciążenia i pomiarów).
Rozważmy teraz Twój przykład:
Po odebraniu żądania dotyczącego akcji indeksu wątek jest rysowany z puli wątków w celu wykonania jego treści, ale jego treść planuje tylko nowe zadanie przy użyciu TPL . Tak więc wykonywanie akcji kończy się, a wątek jest zwracany do puli wątków. Poza tym TPL używa wątków z puli wątków do wykonywania ich przetwarzania. Więc nawet jeśli oryginalny wątek został zwrócony do puli wątków, narysowano inny wątek z tej puli w celu wykonania treści zadania. Więc naraziłeś 2 nici ze swojej cennej puli.
Rozważmy teraz następujące kwestie:
W tym przypadku ręcznie tworzymy wątek. W takim przypadku wykonanie treści akcji Indeks może potrwać nieco dłużej (ponieważ utworzenie nowego wątku jest droższe niż pobranie wątku z istniejącej puli). Ale wykonanie zaawansowanej operacji rejestrowania zostanie wykonane w wątku, który nie jest częścią puli. Dlatego nie zagrażamy wątkom z puli, które pozostają wolne do obsługi innych żądań.
źródło
System.Threading.Task
) uruchomione wewnątrzIndexAsync
metody. W ramach tych operacji wykonujemy wywołania bazy danych do serwera. Więc wszystkie z nich wymagają intensywnych operacji we / wy, prawda? W takim przypadku, czy tworzymy 4 oddzielne wątki (lub otrzymujemy 4 oddzielne wątki z puli wątków)? Zakładając, że mam maszynę wielordzeniową, które będą działać równolegle, prawda?SqlCommand.ExecuteReader
marnujesz wszystko, ponieważ jest to połączenie blokujące. Blokujesz wątek, na którym jest wykonywane to wywołanie, a jeśli ten wątek jest wątkiem z puli, jest to bardzo złe. Zyskasz tylko jeśli używasz I / O Ports Zakończenie:SqlCommand.BeginExecuteReader
. Jeśli nie używasz IOCP bez względu na to, co robisz, nie używaj kontrolerów asynchronicznych, ponieważ wyrządzisz więcej szkód niż zyskasz na ogólnej wydajności aplikacji._context.Foo
, tak naprawdę nic nie wykonujesz. Po prostu budujesz drzewo wyrażeń. Bądź z tym bardzo ostrożny. Wykonanie zapytania jest odroczone tylko wtedy, gdy zaczniesz wyliczanie na zestawie wyników. A jeśli zdarzy się to w widoku, może to być katastrofalne dla wydajności. Aby chętnie wykonać zapytanie EF.ToList()
na końcu.Tak - wszystkie wątki pochodzą z puli wątków. Twoja aplikacja MVC jest już wielowątkowa, gdy żądanie pojawi się w nowym wątku, zostanie pobrany z puli i użyty do obsługi żądania. Ten wątek będzie „zablokowany” (z powodu innych żądań), dopóki żądanie nie zostanie w pełni obsłużone i zakończone. Jeśli w puli nie ma dostępnego wątku, żądanie będzie musiało poczekać, aż jeden będzie dostępny.
Jeśli masz kontrolery asynchroniczne, nadal otrzymują wątek z puli, ale podczas obsługi żądania mogą zrezygnować z wątku, czekając, aż coś się wydarzy (i ten wątek może zostać przekazany do innego żądania) i kiedy pierwotne żądanie wymaga wątku znowu dostaje jeden z puli.
Różnica polega na tym, że jeśli masz wiele długotrwałych żądań (gdzie wątek czeka na odpowiedź), możesz 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 wątki, które oczekują, są zwracane do puli i mogą obsługiwać inne żądania.
Prawie prawdziwym przykładem życia ... Pomyśl o tym jak się w autobusie, tam pięć osób oczekujących, aby dostać się na pierwszy wsiada, płaci i siada (kierowca serwisowane ich prośbę), można dostać się na (kierowca serwisowania Twoja prośba), ale nie możesz znaleźć swoich pieniędzy; jak będziesz grzebać w kieszeniach, kierowca rezygnuje z Ciebie i wciąga kolejne dwie osoby (obsługa ich zgłoszeń), gdy znajdziesz swoje pieniądze, kierowca znowu zaczyna się z Tobą zajmować (realizuje Twoją prośbę) - piąta osoba musi czekać do skończyłeś, ale trzecia i czwarta osoba została obsłużona, gdy byłeś w połowie obsłużenia. Oznacza to, że kierowca jest jedynym wątkiem z puli, a pasażerami są prośby. Pisanie, jak by to działało, gdyby było dwóch kierowców, było zbyt skomplikowane, ale możesz sobie wyobrazić ...
Bez kontrolera asynchronicznego pasażerowie za Tobą musieliby czekać wieki, aż szukałeś swoich pieniędzy, a kierowca autobusu nie pracowałby.
Wniosek jest taki, że jeśli wiele osób nie wie, gdzie są ich pieniądze (tj. Potrzebują dużo czasu, aby odpowiedzieć na coś, o co poprosił kierowca), kontrolery asynchroniczne mogą również pomóc w przepustowości żądań, przyspieszając ten proces od niektórych. Bez kontrolera aysnc wszyscy czekają, aż osoba z przodu zostanie całkowicie rozwiązana. ALE nie zapominaj, że w MVC masz wielu kierowców autobusów w jednym autobusie, więc asynchronizacja nie jest automatycznym wyborem.
źródło
W grę wchodzą dwie koncepcje. Przede wszystkim możemy sprawić, by nasz kod działał równolegle, aby wykonywać go szybciej lub zaplanować kod w innym wątku, aby użytkownik nie musiał czekać. Przykład, który miałeś
należy do drugiej kategorii. Użytkownik uzyska szybszą odpowiedź, ale całkowite obciążenie serwera jest większe, ponieważ musi on wykonywać tę samą pracę + obsługiwać wątki.
Innym przykładem może być:
Ponieważ żądania są wykonywane równolegle, użytkownik nie będzie musiał czekać tak długo, jak gdyby były wykonywane szeregowo. Ale powinieneś zdać sobie sprawę, że zużywamy tutaj więcej zasobów, niż gdybyśmy uruchamiali szeregowo, ponieważ uruchamiamy kod w wielu wątkach, podczas gdy mamy również wątek oczekujący.
W przypadku klienta jest to całkowicie w porządku. Często zdarza się, że synchroniczny, długo działający kod jest zawijany w nowe zadanie (uruchamianie go w innym wątku), aby interfejs użytkownika był responsywny lub paralizowany, aby przyspieszyć. Jednak wątek jest nadal używany przez cały czas trwania. Na serwerze z dużym obciążeniem może to przynieść odwrotny skutek, ponieważ w rzeczywistości zużywa się więcej zasobów. Przed tym ludzie cię ostrzegali
Kontrolery asynchroniczne w MVC mają jednak inny cel. Chodzi o to, aby uniknąć sytuacji, w których nic nie robi nic (co może zaszkodzić skalowalności). Naprawdę ma to znaczenie tylko wtedy, gdy wywoływany interfejs API ma metody asynchroniczne. Podobnie jak WebClient.DowloadStringAsync ().
Chodzi o to, że możesz pozwolić, aby Twój wątek został zwrócony do obsługi nowych żądań, dopóki żądanie sieciowe nie zostanie zakończone, w którym wywoła wywołanie zwrotne, 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 znajduje się twój wątek i czekaj na wynik. Podczas gdy kod asynchroniczny to kod, w którym zostaniesz powiadomiony, gdy kod zostanie ukończony i możesz wrócić do pracy nad nim, w międzyczasie wątek może wykonać inną pracę.
źródło
Aplikacje można lepiej skalować, 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 jest w toku. ASP.NET ma model asynchroniczny, który umożliwia wykonywanie wielu żądań równolegle. Możliwe byłoby kolejkowanie żądań i przetwarzanie ich w trybie FIFO, ale nie byłoby to dobrze skalowane, gdy w kolejce są setki żądań, a przetworzenie każdego żądania zajmuje 100 ms.
Jeśli masz ogromną ilość ruchu, to może lepiej nie wykonywania zapytań asynchronicznie, ponieważ nie może być żadnych dodatkowych zasobów do obsługi żądań . Jeśli nie ma wolnych zasobów, żądania są zmuszane do kolejkowania, trwają wykładniczo dłużej lub wręcz kończą się niepowodzeniem, w którym to przypadku asynchroniczne obciążenie (muteksy i operacje przełączania kontekstu) niczego nie daje.
Jeśli chodzi o ASP.NET, nie masz wyboru - używa modelu asynchronicznego, ponieważ to ma sens w modelu serwer-klient. Jeśli miałbyś pisać własny kod wewnętrznie, który używa wzorca asynchronicznego, aby spróbować lepiej skalować, chyba że próbujesz zarządzać zasobem współdzielonym między wszystkimi żądaniami, w rzeczywistości nie zobaczysz żadnych ulepszeń, ponieważ są one już opakowane w procesie asynchronicznym, który nie blokuje niczego innego.
Ostatecznie wszystko jest subiektywne, dopóki nie przyjrzysz się temu, co powoduje wąskie gardło w twoim systemie. Czasami jest oczywiste, gdzie pomoże wzorzec asynchroniczny (zapobiegając blokowaniu zasobów w kolejce). Ostatecznie tylko pomiar i analiza systemu może wskazać, gdzie można zwiększyć wydajność.
Edytować:
W tym przykładzie
Task.Factory.StartNew
wywołanie spowoduje kolejkowanie operacji w puli wątków platformy .NET. Wątki puli wątków mają być ponownie wykorzystywane (aby uniknąć kosztów tworzenia / niszczenia wielu wątków). Po zakończeniu operacji wątek jest zwalniany z powrotem do puli, aby mógł zostać ponownie użyty przez inne żądanie (moduł Garbage Collector w rzeczywistości nie jest zaangażowany, chyba że utworzyłeś jakieś obiekty w swoich operacjach, w którym to przypadku są one zbierane w normalny sposób zakres).Jeśli chodzi o ASP.NET, nie ma tutaj specjalnej operacji. Żądanie ASP.NET kończy się bez względu na zadanie asynchroniczne. Jedynym problemem może być to, czy twoja pula wątków jest nasycona (tj. 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 którym to przypadku żądanie jest blokowane w oczekiwaniu na uruchomienie zadanie, aż wątek puli stanie się dostępny.
źródło
Task.Factory.StartNew
call ustawia kolejkę operacji w puli wątków .NET. . W tym kontekście, który z nich jest poprawny: 1-) Tworzy nowy wątek, a po zakończeniu wątek wraca do puli wątków i czeka na ponowne użycie. 2-) Pobiera wątek z puli wątków i wraca do puli wątków i czeka na ponowne użycie. 3-) Przyjmuje najbardziej efektywne podejście i może zrobić dowolne z nich.Tak, używają wątku z puli wątków. W witrynie MSDN istnieje całkiem doskonały przewodnik, który odpowie na wszystkie Twoje pytania i nie tylko. W przeszłości okazało się, że jest to bardzo przydatne. Sprawdź to!
http://msdn.microsoft.com/en-us/library/ee728598.aspx
Tymczasem komentarze + sugestie, które słyszysz o kodzie asynchronicznym, należy traktować z przymrużeniem oka. Na początek samo zrobienie czegoś asynchronicznego niekoniecznie poprawia skalowanie, aw niektórych przypadkach może pogorszyć skalowanie aplikacji. Drugi komentarz, który opublikowałeś na temat „dużego natężenia ruchu ...” również jest poprawny tylko w niektórych kontekstach. To naprawdę zależy od tego, co robią Twoje operacje i jak wchodzą w interakcje z innymi częściami systemu.
Krótko mówiąc, wiele osób ma wiele opinii na temat asynchroniczności, ale mogą one nie być poprawne poza kontekstem. Powiedziałbym, że skup się na swoich konkretnych problemach i wykonaj podstawowe testy wydajności, aby zobaczyć, jakie kontrolery asynchroniczne itp. Faktycznie obsługują twoją aplikację.
źródło
GC
ich zakończeniu , przychodzi i czyści je zaraz po zakończeniu, aby moja aplikacja nie miała wycieku pamięci, i tak dalej. Każdy pomysł w tej sprawie.Po pierwsze, to nie MVC, ale IIS, który utrzymuje pulę wątków. Tak więc każde żądanie przychodzące do aplikacji MVC lub ASP.NET jest obsługiwane z wątków utrzymywanych w puli wątków. Dopiero po utworzeniu aplikacji Asynch wywołuje tę akcję w innym wątku i natychmiast zwalnia wątek, aby można było przyjąć inne żądania.
Wyjaśniłem to samo w szczegółowym filmie ( http://www.youtube.com/watch?v=wvg13n5V0V0/ „Kontrolery MVC Asynch i głód wątków”), który pokazuje, jak głód wątków zachodzi w MVC i jak jest minimalizowany przez użycie MVC Kontrolery asynchroniczne Zmierzyłem również kolejki żądań za pomocą perfmon, dzięki czemu możesz zobaczyć, jak kolejki żądań są zmniejszane dla asynchronizacji MVC i jak najgorsze dla operacji synchronizacji.
źródło