Le operazioni asincrone in ASP.NET MVC utilizzano un thread da ThreadPool su .NET 4

Dopo questa domanda, mi fa comodo usare le operazioni asincrone in ASP.NET MVC. Quindi, ho scritto due post di blog su questo:

  • Il mio approccio alla programmazione asincrona basata su attività in C # 5.0 e applicazioni Web MVC ASP.NET
  • Chiamate di database asincrone con TAP (Asynchronous Programming Model) basato su attività in ASP.NET MVC 4

Ho troppe incomprensioni nella mia mente riguardo alle operazioni asincrone su ASP.NET MVC.

Sento sempre questa frase: l’ applicazione può scalare meglio se le operazioni vengono eseguite in modo asincrono

E ho sentito anche questo tipo di frasi: se hai un volume enorme di traffico, potrebbe essere meglio non eseguire le query in modo asincrono: consumare 2 thread aggiuntivi per servire una richiesta porta via le risorse da altre richieste in entrata.

Penso che quelle due frasi siano incoerenti.

Non ho molte informazioni su come funziona threadpool su ASP.NET ma so che threadpool ha una dimensione limitata per i thread. Quindi, la seconda frase deve essere correlata a questo problema.

E vorrei sapere se le operazioni asincrone in ASP.NET MVC utilizzano una discussione da ThreadPool su .NET 4?

Ad esempio, quando implementiamo un controller Async, come vengono strutturate le app? Se ricevo traffico enorme, è una buona idea implementare AsyncController?

C’è qualcuno là fuori che può togliere questa tenda nera davanti ai miei occhi e spiegarmi l’accordo sull’asincronia su ASP.NET MVC 3 (NET 4)?

Modificare:

Ho letto questo documento di seguito quasi centinaia di volte e capisco l’accordo principale, ma ho ancora confusione perché ci sono troppi commenti incoerenti là fuori.

Utilizzo di un controller asincrono in ASP.NET MVC

Modificare:

Supponiamo di avere un’azione del controller come di seguito (non un’implementazione di AsyncController ):

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

Come vedi qui, sparo un’operazione e me ne dimentichi. Quindi, torno immediatamente senza aspettare che venga completato.

In questo caso, deve utilizzare una discussione da threadpool? Se è così, dopo il completamento, cosa succede a quel thread? GC entra e ripulisce subito dopo il completamento?

Modificare:

Per la risposta di @ Darin, ecco un esempio di codice asincrono che comunica con il database:

 public class FooController : AsyncController { //EF 4.2 DbContext instance MyContext _context = new MyContext(); public void IndexAsync() { AsyncManager.OutstandingOperations.Increment(3); Task<IEnumerable>.Factory.StartNew(() => { return _context.Foos; }).ContinueWith(t => { AsyncManager.Parameters["foos"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); Task<IEnumerable>.Factory.StartNew(() => { return _context.Bars; }).ContinueWith(t => { AsyncManager.Parameters["bars"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); Task<IEnumerable>.Factory.StartNew(() => { return _context.FooBars; }).ContinueWith(t => { AsyncManager.Parameters["foobars"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); } public ViewResult IndexCompleted( IEnumerable foos, IEnumerable bars, IEnumerable foobars) { //Do the regular stuff and return } } 

Ecco un eccellente articolo che consiglierei di leggere per comprendere meglio l’elaborazione asincrona in ASP.NET (che è quello che i controller asincroni rappresentano in sostanza).

Consideriamo innanzitutto un’azione sincrona standard:

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

Quando viene effettuata una richiesta per questa azione, viene estratto un thread dal pool di thread e il corpo di questa azione viene eseguito su questo thread. Quindi, se l’elaborazione all’interno di questa azione è lenta, stai bloccando questo thread per l’intera elaborazione, quindi questo thread non può essere riutilizzato per elaborare altre richieste. Al termine dell’esecuzione della richiesta, il thread viene restituito al pool di thread.

Ora prendiamo un esempio del modello asincrono:

 public void IndexAsync() { // perform some processing } public ActionResult IndexCompleted(object result) { return View(); } 

Quando una richiesta viene inviata all’azione Index, viene estratto un thread dal pool di thread e viene eseguito il corpo del metodo IndexAsync . Una volta che il corpo di questo metodo termina l’esecuzione, il thread viene restituito al pool di thread. Quindi, utilizzando AsyncManager.OutstandingOperations standard, dopo aver segnalato il completamento dell’operazione asincrona, viene estratto un altro thread dal pool di thread e viene eseguito il corpo dell’azione IndexCompleted e il risultato IndexCompleted al client.

Quindi quello che possiamo vedere in questo modello è che una richiesta HTTP client singolo potrebbe essere eseguita da due thread differenti.

Ora la parte interessante si verifica all’interno del metodo IndexAsync . Se si dispone di un’operazione di blocco al suo interno, si sta completamente sprecando l’intero scopo dei controller asincroni perché si sta bloccando il thread di lavoro (ricordare che il corpo di questa azione viene eseguito su un thread estratto dal pool di thread).

Quindi, quando possiamo trarre un vantaggio reale dai controller asincroni che potresti chiedere?

IMHO possiamo ottenere di più quando abbiamo operazioni di I / O intensive (come database e chiamate di rete a servizi remoti). Se si dispone di un’operazione intensiva della CPU, le azioni asincrone non offrono molti vantaggi.

Quindi, perché possiamo trarre beneficio dalle operazioni intensive di I / O? Perché potremmo usare I / O Completion Ports . IOCP sono estremamente potenti perché non si consumano thread o risorse sul server durante l’esecuzione dell’intera operazione.

Come funzionano?

Supponiamo di voler scaricare il contenuto di una pagina Web remota utilizzando il metodo WebClient.DownloadStringAsync . Si chiama questo metodo che registrerà un IOCP all’interno del sistema operativo e restituirà immediatamente. Durante l’elaborazione dell’intera richiesta, nessun thread è stato consumato sul tuo server. Tutto accade sul server remoto. Questo potrebbe richiedere molto tempo, ma non ti interessa perché non stai mettendo a repentaglio i tuoi thread di lavoro. Una volta ricevuta una risposta, viene segnalato l’IOCP, viene estratto un thread dal pool di thread e il callback viene eseguito su questo thread. Ma come puoi vedere, durante l’intero processo, non abbiamo monopolizzato alcun thread.

Lo stesso vale per metodi come FileStream.BeginRead, SqlCommand.BeginExecute, …

Che ne dici di parallelizzare più chiamate di database? Si supponga di avere un’azione del controller sincrono in cui sono state eseguite 4 chiamate al database di blocco in sequenza. È facile calcolare che se ogni chiamata al database richiede 200ms, l’azione del controller richiederà circa 800ms.

Se non è necessario eseguire queste chiamate in sequenza, la parallelizzazione migliorerà le prestazioni?

Questa è la grande domanda, a cui non è facile rispondere. Forse sì forse no. Dipenderà interamente da come si implementano tali chiamate al database. Se si utilizzano i controller asincroni e le porte di completamento I / O come discusso in precedenza, si aumenteranno le prestazioni di questa azione del controller e di altre azioni, poiché non si monopolizzerà i thread di lavoro.

D’altra parte se li implementate male (con una chiamata al database bloccante eseguita su un thread dal pool di thread), in pratica ridurrete il tempo totale di esecuzione di questa azione a circa 200ms, ma avreste consumato 4 thread di lavoro in modo da potrebbe aver peggiorato le prestazioni di altre richieste che potrebbero diventare affamate a causa di thread mancanti nel pool per elaborarli.

Quindi è molto difficile e se non ti senti pronto a eseguire test approfonditi sulla tua applicazione, non implementare controller asincroni, poiché è probabile che tu faccia più danni che benefici. Implementale solo se hai un motivo per farlo: ad esempio hai identificato che le azioni standard del controller sincrono sono un collo di bottiglia per l’applicazione (dopo aver eseguito test di carico estesi e misurazioni ovviamente).

Ora consideriamo il tuo esempio:

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

Quando viene ricevuta una richiesta per l’azione Index, viene estratto un thread dal pool di thread per eseguirne il corpo, ma il suo corpo pianifica solo una nuova attività utilizzando TPL . Quindi l’esecuzione dell’azione termina e il thread viene restituito al pool di thread. Tranne che, TPL utilizza thread dal pool di thread per eseguire la loro elaborazione. Quindi, anche se il thread originale è stato restituito al pool di thread, è stato disegnato un altro thread da questo pool per eseguire il corpo dell’attività. Quindi hai messo a rischio 2 fili dalla tua preziosa piscina.

Ora consideriamo quanto segue:

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

In questo caso stiamo generando manualmente una discussione. In questo caso, l’esecuzione del corpo dell’azione indice potrebbe richiedere un po ‘più tempo (perché la generazione di un nuovo thread è più costosa rispetto a quella di un pool esistente). Ma l’esecuzione dell’operazione di registrazione avanzata verrà eseguita su un thread che non fa parte del pool. Quindi non stiamo mettendo a repentaglio i thread dal pool che rimangono liberi per servire altre richieste.

Sì, tutti i thread provengono dal pool di thread. La tua app MVC è già multi-thread, quando una richiesta arriva in una nuova discussione verrà presa dal pool e utilizzata per soddisfare la richiesta. Quella discussione sarà ‘bloccata’ (da altre richieste) fino a quando la richiesta non sarà completamente revisionata e completata. Se non ci sono thread disponibili nel pool, la richiesta dovrà attendere fino a quando uno sarà disponibile.

Se si hanno controller asincroni, ottengono comunque un thread dal pool ma durante la manutenzione della richiesta possono abbandonare il thread, mentre si attende che qualcosa accada (e che il thread possa essere assegnato a un’altra richiesta) e quando la richiesta originale richiede un thread di nuovo ne prende uno dalla piscina.

La differenza è che se si hanno molte richieste di lunga durata (in cui il thread è in attesa di una risposta da qualcosa) si potrebbero esaurire i thread dal pool per soddisfare anche le richieste di base. Se si dispone di controller asincroni, non si hanno più thread ma i thread in attesa vengono restituiti al pool e possono servire altre richieste.

Un esempio di vita quasi reale … Pensa che è come salire su un autobus, ci sono cinque persone che aspettano di salire, il primo sale, paga e si mette a sedere (l’autista ha revisionato la richiesta), vai avanti (l’autista sta riparando la tua richiesta) ma non riesci a trovare i tuoi soldi; mentre stai armeggiando nelle tue tasche l’autista ti arrende e prende le due persone successive (rispondendo alle loro richieste), quando trovi i tuoi soldi l’autista ricomincia a trattare con te (completando la tua richiesta) – la quinta persona deve aspettare fino a hai finito ma la terza e la quarta gente sono state servite mentre eri a metà del servizio. Ciò significa che l’autista è l’unico thread del pool ei passeggeri sono le richieste. Era troppo complicato scrivere come avrebbe funzionato se ci fossero due piloti ma puoi immaginare …

Senza un controller asincrono, i passeggeri dietro di te avrebbero dovuto aspettare le età mentre cercavi i tuoi soldi, nel frattempo l’autista non avrebbe fatto nulla.

Quindi la conclusione è che se molte persone non sanno dove sono i loro soldi (cioè richiedono molto tempo per rispondere a qualcosa che il driver ha chiesto) i controller asincroni potrebbero aiutare a velocizzare il trasferimento delle richieste, accelerando il processo da alcuni. Senza un controller aysnc tutti aspettano fino a quando la persona di fronte è stata completamente gestita. MA non dimenticare che in MVC ci sono molti driver di bus su un singolo bus, quindi async non è una scelta automatica.

Ci sono due concetti in gioco qui. Prima di tutto possiamo far funzionare il nostro codice in parallelo per eseguire più velocemente o programmare il codice su un altro thread per evitare che l’utente attenda. L’esempio che hai avuto

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

appartiene alla seconda categoria. L’utente otterrà una risposta più veloce ma il carico di lavoro totale sul server è più alto perché deve fare lo stesso lavoro + gestire il threading.

Un altro esempio di questo sarebbe:

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

Poiché le richieste vengono eseguite in parallelo, l’utente non dovrà attendere fino a quando viene eseguito in seriale. Ma dovresti renderci conto che usiamo più risorse qui che se usassimo in seriale perché eseguiamo il codice su molti thread mentre stiamo aspettando il thread.

Questo è perfettamente soddisfacente in uno scenario client. Ed è abbastanza comune che avvolga il codice sincrono a lunga esecuzione in una nuova attività (eseguilo su un altro thread) e tieni l’ui reattivo o parallelo per renderlo più veloce. Un thread è ancora usato per l’intera durata però. Su un server con un carico elevato, questo potrebbe ritorcersi contro il fuoco perché si utilizzano effettivamente più risorse. Questo è ciò di cui le persone ti hanno messo in guardia

I controller asincroni in MVC hanno comunque un altro objective. Il punto qui è di evitare di far passare le sedute senza fare nulla (che può danneggiare la scalabilità). Importa davvero solo se le API che stai chiamando hanno metodi asincroni. Come WebClient.DowloadStringAsync ().

Il punto è che puoi lasciare che il tuo thread sia restituito per gestire nuove richieste fino a quando la richiesta web non è terminata, dove chiamerà te callback che ottiene lo stesso o un nuovo thread e completa la richiesta.

Spero tu capisca la differenza tra asincrono e parallelo. Pensa al codice parallelo come codice in cui si trova il thread e attendi il risultato. Mentre il codice asincrono è il codice in cui verrai avvisato quando il codice è terminato e potrai tornare a lavorarci, nel frattempo il thread può fare altro lavoro.

Le applicazioni possono scalare meglio se le operazioni vengono eseguite in modo asincrono, ma solo se sono disponibili risorse per il servizio delle operazioni aggiuntive .

Le operazioni asincrone assicurano che non si stia bloccando un’azione perché è in corso una esistente. ASP.NET ha un modello asincrono che consente a più richieste di eseguire side-by-side. Sarebbe ansible accodare le richieste e elaborarle in FIFO, ma questo non sarebbe scalabile quando centinaia di richieste vengono messe in coda e ogni richiesta richiede 100 ms per l’elaborazione.

Se si dispone di un volume enorme di traffico, potrebbe essere preferibile non eseguire le query in modo asincrono, in quanto potrebbero non esserci risorse aggiuntive per soddisfare le richieste . Se non ci sono risorse di riserva, le tue richieste sono costrette a fare la fila, a fare esponenzialmente un errore più lungo o definitivo, nel qual caso il sovraccarico asincrono (mutex e operazioni di commutazione di contesto) non ti dà nulla.

Per quanto riguarda ASP.NET, non hai scelta: utilizza un modello asincrono, perché è quello che ha senso per il modello client-server. Se dovessi scrivere internamente il tuo codice che utilizza uno schema asincrono per tentare di scalare meglio, a meno che tu non stia provando a gestire una risorsa condivisa tra tutte le richieste, in realtà non vedrai alcun miglioramento perché sono già stati completati in un processo asincrono che non blocca qualcos’altro.

In definitiva, è tutto soggettivo fino a quando non si esamina effettivamente ciò che causa un collo di bottiglia nel sistema. A volte è ovvio dove un pattern asincrono aiuterà (impedendo un blocco delle risorse in coda). In definitiva, solo la misurazione e l’analisi di un sistema possono indicare dove è ansible ottenere efficienza.

Modificare:

Nell’esempio, la chiamata Task.Factory.StartNew effettuerà l’accodamento di un’operazione sul pool di thread .NET. La natura dei thread del pool di thread deve essere riutilizzata (per evitare il costo di creazione / distruzione di molti thread). Una volta completata l’operazione, il thread viene rilasciato nuovamente nel pool per essere riutilizzato da un’altra richiesta (il Garbage Collector in realtà non viene coinvolto a meno che non si siano creati alcuni oggetti nelle operazioni, nel qual caso vengono raccolti come da normale scoping).

Per quanto riguarda ASP.NET, non ci sono operazioni speciali qui. La richiesta ASP.NET viene completata senza rispettare l’attività asincrona. L’unica preoccupazione potrebbe essere se il pool di thread è saturo (cioè non ci sono thread disponibili per servire la richiesta in questo momento e le impostazioni del pool non consentono la creazione di più thread), nel qual caso la richiesta è bloccata in attesa di avviare il attività fino a quando un thread di pool diventa disponibile.

Sì, usano una discussione dal pool di thread. C’è in realtà una guida piuttosto eccellente da MSDN che affronterà tutte le tue domande e altro ancora. Ho trovato che sia abbastanza utile in passato. Controlla!

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

Nel frattempo, i commenti + suggerimenti che si sentono sul codice asincrono dovrebbero essere presi con un pizzico di sale. Per cominciare, solo fare qualcosa di asincrono non lo rende necessariamente più scalabile, e in alcuni casi può peggiorare le dimensioni dell’applicazione. Anche l’altro commento che hai postato su “un enorme volume di traffico …” è corretto solo in determinati contesti. Dipende davvero da cosa stanno facendo le tue operazioni e da come interagiscono con altre parti del sistema.

In breve, molte persone hanno molte opinioni su async, ma potrebbero non essere corrette fuori dal contesto. Direi di concentrarsi sui vostri problemi esatti e fare test di base sulle prestazioni per vedere quali controller asincrono, ecc. In realtà gestiscono con la vostra applicazione.

Per prima cosa non è MVC ma IIS che gestisce il pool di thread. Quindi ogni richiesta che arriva a MVC o all’applicazione ASP.NET viene fornita da thread che vengono mantenuti nel pool di thread. Solo con la creazione dell’app Asynch richiama questa azione in un thread diverso e rilascia immediatamente il thread in modo che possano essere prese altre richieste.

Ho spiegato la stessa cosa con un video di dettaglio ( http://www.youtube.com/watch?v=wvg13n5V0V0/ “MVC Asynch controller e thread starvation”) che mostra come la fame di thread si verifica in MVC e il modo in cui viene ridotta a icona utilizzando MVC Controllori Asynch. Ho anche misurato le code di richieste usando perfmon in modo da poter vedere come le code di richieste siano diminuite per l’asincrono MVC e come sia il peggiore per le operazioni di sincronizzazione.