Originale:Il login del link ipertestuale è visibile.
Sommario: Anche se pensi di sapere cosa stai facendo, non è sicuro memorizzare nulla in un membro ThreadStatic, CallContext o Thread Local in un'applicazione ASP.Net se il valore è stato impostato in anticipo. a Page_Load (ad esempio in IHttpModule o nel costruttore di pagina), ma durante o dopo l'accesso.
[Aggiornamento: agosto 2008 Dato che molte persone continuano a linkare questo articolo, ritengo necessario chiarire che questo comportamento di scambio di thread avviene in un punto molto specifico del ciclo di vita della pagina, e non quando ne ha bisogno.] La mia formulazione dopo aver citato Jeff Newsom è sfortunata. A parte questo, sono molto felice (e lusingato) di vedere più volte riferimenti a questo articolo nelle discussioni di design sulla corretta gestione di HttpContext. Sono contento che la gente lo trovi utile. ]
C'è molta confusione su come implementare singleton specifici per utente in ASP.Net - cioè, i dati globali sono globali per un solo utente o richiesta. Questo non è un requisito raro: pubblicare dati transazionali, di contesto di sicurezza o altri dati "globali" in un unico luogo, invece di spingerli in ogni chiamata di metodo, perché i dati loam permettono un'implementazione più chiara (e più leggibile). Tuttavia, se non stai attento, questo è un ottimo posto per spararti in un piede (o in testa). Pensavo di sapere cosa stesse succedendo, ma non era così.
L'opzione preferita sarà singletonMemorizzato in HttpContext.Current.Items, semplice e sicuro, ma collegando il singolare in questione al suo utilizzo nella ASP.Net applicazione. Se il singleton fallisce nel tuo oggetto aziendale, allora questo non è l'ideale. Anche se si avvolge property access in un'istruzione if
Summary: Anche se pensi di sapere cosa stai facendo, non è sicuro memorizzare nulla in un membro ThreadStatic, CallContext o Thread Local Storage all'interno di un'applicazione ASP.Net, se c'è la possibilità che il valore può essere impostato prima di Page_Load (ad esempio in IHttpModule o page constructor) ma accessibile durante o dopo.
[Aggiornamento: agosto 2008 Considerando il numero piuttosto elevato di persone che continuano a linkare questo post, sento il bisogno di chiarire che questo comportamento di cambio di thread avviene in un punto molto specifico della pagina Ciclo di vita e non quando ti pare. La mia formulazione dopo la citazione di Jef Newson è stata sfortunata. A parte questo, sono stato immensamente soddisfatto (e lusingato) dal numero di volte in cui ho visto questo post citato nelle discussioni di design su come gestire in modo appropriato HttpContext. Sono contento che la gente l'abbia trovato utile.]
C'è molta confusione sull'uso di come implementare singleton specifici per utente in ASP.Net - cioè dati globali che sono globali solo per un utente o una richiesta. Questo non è un requisito raro: pubblicare Transazioni, contesto di sicurezza o altri dati 'globali' in un unico luogo, invece di inviarli attraverso ogni chiamata di metodo come i dati tramp possono creare un un'implementazione più pulita (e leggibile). Tuttavia, è un ottimo posto per spararsi nel piede (o in testa) se non stai attento. Pensavo di sapere cosa stesse succedendo, ma non era così.
L'opzione preferita, memorizzare i singleton in HttpContext.Current.Items, è semplice e sicura, ma collega il singleton in questione all'uso all'interno di un'applicazione ASP.Net. Se il singleton è in calo nei tuoi oggetti aziendali, non è l'ideale. Anche se si avvolge property-access in un'istruzione if
... così devi comunque fare riferimento a System.Web da quell'assemblaggio, che tende a incoraggiare più oggetti 'webby' nel posto sbagliato.
Le alternative sono usare un membro statico [ThreadStatic], lo storage locale Thread (che equivale praticamente alla stessa cosa) o CallContext.
I problemi con [ThreadStatic] sono ben documentati, ma per riassumere: Gli initalizzatori di campo sparano solo sul primo thread I dati ThreadStatic necessitano di una pulizia esplicita (ad esempio in EndRequest), perché mentre il Thread è raggiungibile, i dati ThreadStatic non verranno GC quindi potresti perdere risorse. I dati ThreadStatic sono utili solo all'interno di una richiesta, perché la richiesta successiva potrebbe arrivare su un thread diverso e ottenere i dati di qualcun altro. Scott Hanselman ha ragione, dicendo che ThreadStatic non funziona bene con ASP.Net, ma non spiega completamente il perché.
Lo storage in CallContext allevia alcuni di questi problemi, poiché il contesto muore alla fine della richiesta e il GC si verificherà prima o poi (anche se puoi comunque perdere risorse finché il GC non si verifica se stai conservando i Usa e Getta). Inoltre, CallContext è il modo in cui viene memorizzato HttpContext, quindi deve andare bene, giusto? In ogni caso, penseresti (come ho fatto io) che, a patto che alla fine di ogni richiesta ti pulissi tutto bene: "Se iniziali la variabile ThreadStatic all'inizio della richiesta e gestisci correttamente l'oggetto di riferimento alla fine della richiesta, rischierei di affermare che non succederà nulla di male
"Ora,Potrei sbagliarmi. CLR potrebbe smettere di ospitare un thread a metà, serializzare il suo stack da qualche parte, dargli uno nuovo stack e lasciarlo iniziare a eseguire。 Ne dubito profondamente. Immagino sia concepibile che anche l'hyperthreading renderebbe le cose difficili, ma ne dubito anche. ”
Jeff Newsom
"Se iniziali una variabile ThreadStatic all'inizio di una richiesta, e elimini correttamente l'oggetto di riferimento alla fine della richiesta, mi arrisco a dichiarare che nulla Succederà il male. Sei persino a posto tra contesti nello stesso AppDomain
"Ora, potrei sbagliarmi su questo. Il clr potrebbe potenzialmente fermare un thread gestito a metà streaming, serializzare il suo stack da qualche parte, dargli uno nuovo stack e lasciarlo iniziare a eseguire. Ne dubito seriamente. Suppongo sia concepibile che anche l'hyperthreading renda le cose difficili, ma ne dubito anche." Jef Newsom Aggiornamento: Questo è fuorviante. Spiegherò meglio più avanti che questo cambio di thread può avvenire solo tra BeginRequest e Page_Load, ma il riferimento di Jef crea un'immagine molto potente che non sono riuscito a correggere subito.
Update: This was the misleading bit. I do explain further later on that this thread-swap can only happen between the BeginRequest and the Page_Load, but Jef's quote creates a very powerful image I failed to immediately correct. My bad. Quindi, a un certo punto, ASP.NET decidere che ci sono troppi thread I/O che elaborano altre richieste. […] Accetta solo richieste e le mette in coda in un oggetto interno all'interno dell ASP.NET runtime. Poi, dopo la coda, il thread I/O richiederà un thread lavoratore, e il thread I/O tornerà nel suo pool. […] Quindi ASP.NET lascerà che quel lavoratore gestisca la richiesta. Lo porterà a ASP.NET runtime, proprio come farebbe il thread di I/O a basso carico.
Quindi a un certo punto ASP.NET decide che ci sono troppi thread di I/O che elaborano altre richieste. [...] Prende semplicemente la richiesta e la mette in coda in questo oggetto interno all'interno del runtime ASP.NET. Poi, dopo che questo è stato messo in coda, il thread I/O chiederà un thread lavoratore, e il thread I/O verrà restituito al suo pool. [...] Quindi ASP.NET farà processare la richiesta da quel thread di lavoro. Lo porterà nel runtime ASP.NET, proprio come il thread I/O avrebbe avuto sotto basso carico. Ora, l'ho sempre saputo, ma credo sia successo molto presto e non me ne importava. Tuttavia, sembra che mi sbagli. Abbiamo avuto un problema in ASP.Net'app in cui un utente cliccava su un link dopo averne cliccato un altro, e la nostra app aveva un'eccezione di riferimento nullo in uno dei singleton (io ho usato CallContext invece di ThreadStatic per il singleton, ma si è rivelato irrilevante).
Ho fatto qualche ricerca su come funzionano esattamente ASP.Net thread e ho trovato opinioni contrastanti mascherate da fatti (le richieste sono thread-agile nelle richieste, mentre le richieste sono ancorate ai thread durante la loro vita), quindi ho copiato il mio problema in un'applicazione di test con una pagina lenta (in sospensione per un secondo) e una pagina veloce. Clicco sul link alla pagina lenta e, prima che la pagina torni, clicco sul link alla pagina veloce. Il risultato (il dump di log4net di quello che sta succedendo) mi ha lasciato senza parole.
L'output mostra che per la seconda richiesta, l'evento BeginRequest e il costruttore di pagina nella pipeline HttpModule si attivano su un thread, ma page_Load in un altro. Il secondo thread ha già migrato l'HttpContext dal primo thread, ma non il CallContext o il ThreadStatic (nota: poiché l'HttpContext stesso è memorizzato nel CallContext, questo significa che ASP.Net sta migrando esplicitamente l'HttpContext). Ripetiamolo:
Ora, l'ho sempre saputo, ma pensavo fosse successo abbastanza presto nel processo da non importarmi. Tuttavia, sembra che mi sbagliassi. Abbiamo avuto un problema nella nostra app ASP.Net dove l'utente clicca su un link subito dopo averne cliccato su un altro, e la nostra app si blocca con un'eccezione di riferimento nullo per uno dei nostri singleton (io uso CallContext non ThreadStatic per il singleton, ma a quanto pare non importa).
Ho fatto qualche ricerca su come si usa esattamente l'ASP. Il threading di rete funziona, e ha ricevuto opinioni contrastanti - mascherando-as-fatto (le richieste sono thread-agile all'interno di una richiesta mentre le richieste sono fissate su un thread per tutta la loro vita) quindi ho replicato il mio Problema in un'applicazione di test con una pagina lenta (sospensione per un secondo) e una pagina veloce. Clicco sul link della pagina lenta e, prima che la pagina torni, clicco sul link per la pagina veloce. I risultati (un dump di log4net di ciò che sta succedendo) mi hanno sorpreso.
Quello che mostra l'output è che - per la seconda richiesta - gli eventi BeginRequest nella pipeline HttpModule e il costruttore di pagina si attivano su un thread, ma il Page_Load si attiva su un altro. Il secondo thread ha avuto l'HttpContext migrato dal primo, ma non il CallContext o il ThreadStatic (NB: poiché HttpContext è memorizzato in CallContext, questo significa che ASP.Net è migrando esplicitamente l'HttpContext attraverso di sé). Spieghiamolo di nuovo:
- Il cambio di thread avviene dopo la creazione dell'IHttpHandler
- Dopo l'avvio del campo e l'esecuzione del costruttore della pagina
- Dopo qualsiasi evento di tipo BeginRequest, AuthenticateRequest, AquireSessionState utilizzato da Global.ASA/IHttpModules.
- Solo l'HttpContext viene migrato nel nuovo thread
Il cambio di thread avviene dopo la creazione dell'IHttpHandler Dopo l'esecuzione degli inizializzatori di campo e del costruttore della pagina Dopo qualsiasi evento di tipo BeginRequest, AuthenticateRequest, AquireSessionState che i tuoi moduli Global.ASA / IHttpModules stanno usando. Solo l'HttpContext migra al nuovo thread È una vera seccatura perché, da quello che ho visto, significa che l'unica opzione di persistenza per il comportamento della classe "ThreadStatic" in ASP.Net è usare HttpContext. Quindi per i tuoi oggetti aziendali, o continui a usare if(HttpContext.Current!). =null) e System.Web reference (bleah), oppure devi inventare un modello provider per la persistenza statica, che deve essere configurato prima di accedere a questi singleton. Doppia nausea.
Per favore, dì che non è così.
Appendice: Registro completo:
È una vera seccatura, perché per quanto posso vedere significa che l'unica opzione di persistenza per un comportamento 'ThreadStatic' in ASP.Net è usare HttpContext. Quindi per i tuoi business objects, o sei bloccato con if(HttpContext.Current!=null) e il riferimento System.Web (che schifo) oppure devi inventare un modello provider per il tuo persistenza statica, che dovrà essere impostata prima del momento in cui uno di questi singleton venga accesso. Doppio schifo.
Per favore, qualcuno dica che non è così.
Appendix: That log in full: [3748] INFO 11:10:05,239 ASP. Global_asax. Application_BeginRequest() - START /ConcurrentRequestsDemo/SlowPage.aspx [3748] INFO 11:10:05,239 ASP. Global_asax. Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=97, calldata= [3748] INFO 11:10:05,249 ASP. SlowPage_aspx.. ctor() - threadid=3748, threadhash=(cctor)97, threadhash(ora)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(ora)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - Pagine che si ripidano lentamente....
[2720] INFO 11:10:05,669 ASP. Global_asax. Application_BeginRequest() - START /ConcurrentRequestsDemo/FastPage.aspx [2720] INFO 11:10:05,679 ASP. Global_asax. Application_BeginRequest() - threadid=, threadhash=, threadhash(ora)=1835, calldata= [2720] INFO 11:10:05,679 ASP. FastPage_aspx.. ctor() - threadID=2720, ThreadHash=(CCTOR)1835, ThreadHash(Now)=1835, callData=2720, LogicCallData=2720, Threadstatic=2720
[3748] INFO 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - Pagina lenta che si sveglia.... [3748] INFO 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(ora)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:06,350 ASP. Global_asax. Application_EndRequest() - threadid=3748, threadhash=97, threadhash(ora)=97, calldata=3748 [3748] INFO 11:10:06,350 ASP. Global_asax. Application_EndRequest() - FINE /ConcurrentRequestsDemo/SlowPage.aspx
[4748] INFO 11:10:06,791 ASP. FastPage_aspx. Page_Load() - threadid=2720, threadhash=(cctor)1835, threadhash(ora)=1703, calldata=, logicalcalldata=, threadstatic= [4748] INFO 11:10:06,791 ASP. Global_asax. Application_EndRequest() - threadid=2720, threadhash=1835, threadhash(ora)=1703, calldata= [4748] INFO 11:10:06,791 ASP. Global_asax. Application_EndRequest() - FINE /ConcurrentRequestsDemo/FastPage.aspx La chiave è cosa succede quando la Page_Load di FastPage viene attivata. Il ThreadID è 4748, ma il ThreadID che ho memorizzato nell'HttpContext di ctor è 2720. Il codice hash per i thread logici è 1703, ma il codice hash che memorizzo nel ctor è 1835. Tutti i dati che ho memorizzato nel CallContext mancano (anche i dati segnati ILogicalThreadAffinnative), ma l'HttpContext è ancora presente. Come ci si può aspettare, anche il mio ThreadStatic è sparito.
La cosa chiave è cosa succede quando la Page_Load di FastPage si attiva. Il ThreadID è 4748, ma il threadID che ho memorizzato in HttpContext nel ctor è 2720. Il codice hash per il thread logico è 1703, ma quello che ho memorizzato nel ctor è 1835. Tutti i dati che ho memorizzato nel CallContext sono spariti (anche quelli contrassegnati come ILogicalThreadAffinative), ma HttpContext è ancora presente. Come ci si aspetterebbe, anche il mio ThreadStatic è sparito. (Fine)
|