Original:Autentificarea cu hyperlink este vizibilă.
Rezumat: Chiar dacă crezi că știi ce faci, nu este sigur să stochezi nimic într-un magazin ThreadStatic member, CallContext sau Thread Local într-o aplicație ASP.Net dacă valoarea a fost setată dinainte. să Page_Load (de exemplu, în IHttpModule sau constructorul de pagini), dar în timpul sau după acces.
[Actualizare: august 2008 Având în vedere că destul de mulți oameni continuă să facă linkuri către acest articol, consider necesar să clarific că acest comportament de schimbare a firului are loc într-un punct foarte specific al ciclului de viață al paginii, și nu atunci când simt nevoie.] Formularea mea după ce l-am citat pe Jeff Newsom este nefericită. În afară de asta, sunt foarte fericit (și flatat) să văd referințe la acest articol de mai multe ori în discuții de design despre gestionarea corectă a HttpContext. Mă bucur că oamenii îl găsesc util. ]
Există multă confuzie cu privire la modul de implementare singleton-uri specifice utilizatorului în ASP.Net – adică, datele globale sunt globale pentru un singur utilizator sau cerere. Aceasta nu este o cerință neobișnuită: publicarea datelor tranzacționale, de context de securitate sau alte date "globale" într-un singur loc, în loc să le împingi în fiecare apel de metodă, deoarece datele loam permit o implementare mai clară (și mai ușor de citit). Totuși, dacă nu ești atent, acesta este un loc excelent pentru a te împușca în picior (sau în cap). Credeam că știu ce se întâmplă, dar nu știam.
Opțiunea preferată va fi singletonStocat în HttpContext.Current.Items, simplu și sigur, dar leagă singletonul în cauză de utilizarea sa în cererea ASP.Net. Dacă singleton-ul eșuează în obiectul tău de afaceri, atunci acest lucru nu este ideal. Chiar dacă înfășori property access într-o instrucțiune if
Summary: Chiar dacă crezi că știi ce faci, nu este sigur să stochezi nimic într-un membru ThreadStatic, CallContext sau Thread Local Storage într-o aplicație ASP.Net, dacă există posibilitatea că valoarea poate fi configurată înainte de Page_Load (de exemplu în IHttpModule sau constructor de pagini), dar accesată în timpul sau după.
[Actualizare: Aug 2008 Având în vedere numărul destul de mare de persoane care continuă să facă link către această postare, simt nevoia să clarific că acest comportament de schimbare a firelor are loc într-un punct foarte specific al paginii Ciclu de viață și nu oricâte ori simt că vrei. Formularea mea după citatul lui Jef Newson a fost nefericită. Lăsând asta la o parte, am fost extrem de mulțumit (și flatat) de câte ori am văzut această postare citată în discuțiile de design despre cum să gestionezi corespunzător HttpContext. Mă bucur că oamenii l-au găsit util.]
Există multă confuzie în privința modului de a implementa singleton-uri specifice utilizatorului în ASP.Net – adică date globale care sunt globale doar pentru un singur utilizator sau cerere. Aceasta nu este o cerință neobișnuită: publicarea Tranzacțiilor, contextului de securitate sau a altor date "globale" într-un singur loc, în loc să le împingi prin fiecare apel de metodă, așa cum pot genera datele tramp o implementare mai curată (și mai ușor de citit). Totuși, este un loc excelent pentru a te împușca în picior (sau în cap) dacă nu ești atent. Credeam că știu ce se întâmplă, dar nu știam.
Opțiunea preferată, stocarea singleton-ilor în HttpContext.Current.Items, este simplă și sigură, dar leagă singletonul în cauză de a fi folosit într-o aplicație ASP.Net. Dacă singletonul este mai mic în obiectele tale de afaceri, acest lucru nu este ideal. Chiar dacă înfășori property-access într-o instrucțiune if
... apoi tot trebuie să faci referire la System.Web din acea asamblare, care tinde să încurajeze mai multe obiecte "webby" în locul greșit.
Alternativele sunt să folosești un membru static [ThreadStatic], stocare locală Thread (care practic înseamnă același lucru) sau CallContext.
Problemele cu [ThreadStatic] sunt bine documentate, dar ca să rezum: Initalizatoarele de câmp se activează doar pe primul fir Datele ThreadStatic necesită curățare explicită (de exemplu în EndRequest), pentru că, deși Thread-ul este accesibil, datele ThreadStatic nu vor fi GC-uite, deci s-ar putea să pierzi resurse. Datele ThreadStatic sunt utile doar într-o cerere, pentru că următoarea cerere poate veni într-un alt fir de discuție și poate obține datele altcuiva. Scott Hanselman spune corect că ThreadStatic nu se potrivește bine cu ASP.Net, dar nu explică pe deplin de ce.
Stocarea în CallContext ameliorează unele dintre aceste probleme, deoarece contextul dispare la finalul cererii și GC va apărea în cele din urmă (deși poți totuși să pierzi resurse până când GC apare dacă depozitezi obiecte de unică folosință). În plus, CallContext este modul în care se stochează HttpContext, deci trebuie să fie în regulă, nu? Oricum, te-ai gândi (așa cum am crezut eu) că, dacă ai strânge după tine la finalul fiecărei cereri, totul ar fi în regulă: "Dacă inițializezi variabila ThreadStatic la începutul cererii și gestionezi corect obiectul referențiat la finalul cererii, aș risca să pretind că nu se va întâmpla nimic rău
"Acum,S-ar putea să mă înșel. CLR poate înceta să găzduiască un fir de discuție la jumătatea drumului, să-și serializeze stiva undeva, să-i dea un nou stack și să-l lase să înceapă să execute。 Mă îndoiesc profund de asta. Cred că este de conceput că hyperthreading-ul ar complica lucrurile, dar mă îndoiesc și de asta. ”
Jeff Newsom
"Dacă inițializați o variabilă ThreadStatic la începutul unei cereri și eliminați corect obiectul referențiat la finalul cererii, voi risca să pretind că nimic Răul se va întâmpla. Ești chiar cool între contexte în același AppDomain
"Acum, aș putea greși în privința asta. CLR ar putea, potențial, să oprească un fir gestionat pe parcurs, să-și serializeze stiva undeva, să-i dea un nou stack și să-l lase să înceapă să execuțe. Mă îndoiesc serios. Presupun că este posibil ca hyperthreading-ul să complice lucrurile, dar mă îndoiesc și de asta." Jef Newsom Actualizare: Este înșelător. Voi explica mai detaliat mai târziu că această schimbare de discuții poate avea loc doar între BeginRequest și Page_Load, dar referința lui Jef creează o imagine foarte puternică pe care nu am reușit să o corectez imediat.
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. Așa că, la un moment dat, ASP.NET decide că sunt prea multe fire de I/O care procesează alte cereri. […] Acceptă doar cereri și le pune în coadă într-un obiect intern în timpul ASP.NET runtime. Apoi, după coadă, firul de I/O va solicita un fir de lucru, iar firul de I/O se va întoarce în pool-ul său. […] Prin urmare, ASP.NET lasă acel angajat să se ocupe de cerere. Îl va aduce la ASP.NET runtime, exact ca thread-ul I/O la sarcină mică.
Așa că, la un moment dat ASP.NET decide că sunt prea multe fire de I/O care procesează alte cereri. [...] Pur și simplu ia cererea și o pune în coadă în acest obiect intern de coadă în timpul ASP.NET runtime. Apoi, după ce acesta este pus în coadă, firul de I/O va cere un fir de muncă, iar firul de I/O va fi returnat în pool-ul său. [...] Așadar ASP.NET va pune acel thread de lucru să proceseze cererea. Îl va prelua în timpul de execuție ASP.NET, la fel cum ar face firul de I/O sub sarcină scăzută. Am știut mereu despre asta, dar cred că s-a întâmplat foarte devreme și nu-mi păsa. Totuși, se pare că mă înșel. Am avut o problemă într-ASP.Net aplicație unde un utilizator a dat click pe un link după ce a dat click pe alt link, iar aplicația noastră avea o excepție de referință nulă într-unul dintre singleton-uri (am folosit CallContext în loc de ThreadStatic pentru singleton, dar s-a dovedit a fi irelevant).
Am făcut niște cercetări despre cum funcționează exact firele ASP.Net și am găsit opinii contradictorii mascate în fapte (cererile sunt agile în fire, în timp ce cererile sunt ancorate pe fire pe durata lor de viață), așa că am copiat problema într-o aplicație de test cu o pagină lentă (care a dormit o secundă) și una rapidă. Dau click pe linkul către pagina lentă, iar înainte ca pagina să revină, dau click pe linkul către pagina rapidă. Rezultatul (dumparea log4net a ceea ce se întâmplă) m-a lăsat fără cuvinte.
Rezultatul arată că pentru a doua cerere, evenimentul BeginRequest și constructorul de pagini din pipeline-ul HttpModule se declanșează pe un fir de execuție, dar page_Load în altul. Al doilea fir a migrat deja HttpContextul din primul fir, dar nu și CallContext sau ThreadStatic (notă: deoarece HttpContextul este stocat în CallContext, asta înseamnă că ASP.Net migrează explicit HttpContextul). Să o spunem din nou:
Am știut mereu despre asta, dar am presupus că s-a întâmplat destul de devreme în proces încât să nu-mi pese. Se pare însă că m-am înșelat. Am avut o problemă în aplicația noastră de ASP.Net unde utilizatorul dă click pe un link imediat după altul, iar aplicația noastră explodează cu o excepție de referință nulă pentru unul dintre singletonii noștri (eu folosesc CallContext, nu ThreadStatic pentru singleton, dar se pare că nu contează).
Am făcut puțină cercetare despre cum anume se face ASP. Threading-ul rețelei funcționează și a generat opinii contradictorii – deghizate în fapte (cererile sunt agile în cadrul unei cereri versus cererile sunt fixate pe un fir pe durata vieții), așa că am replicat O problemă într-o aplicație de test cu o pagină lentă (în repaus) și o pagină rapidă. Dau click pe linkul pentru pagina lentă și, înainte ca pagina să revină, dau click pe linkul pentru pagina rapidă. Rezultatele (o analiză log4net a ceea ce se întâmplă) m-au surprins.
Ceea ce arată rezultatul este că - pentru a doua cerere - evenimentele BeginRequest din pipeline-ul HttpModule și constructorul de pagini pornesc pe un fir de execuție, dar Page_Load pornește pe altul. Al doilea fir a avut HttpContext migrat din primul, dar nu și CallContext sau ThreadStatic (NB: deoarece HttpContext este stocat în CallContext, asta înseamnă ASP.Net este migrarea explicită a HttpContextului). Să explicăm din nou clar:
- Comutarea firului are loc după ce IHttpHandler este creat
- După inițializatorul de câmp și rularea constructorului paginii
- După orice evenimente de tip BeginRequest, AuthenticateRequest, AquireSessionState folosite de Global.ASA/IHttpModules.
- Doar HttpContextul este migrat către noul fir de discuție
Comutatorul firului are loc după ce IHttpHandler a fost creat După ce inițializatoarele câmpului și rularea constructorului paginii După orice evenimente de tip BeginRequest, AuthenticateRequest, AquireSessionState pe care le folosesc modulele tale Global.ASA / IHttp. Doar HttpContext migrează către noul fir de discuție Este o mare bătaie de cap pentru că, din ce am văzut, singura opțiune de persistență pentru comportamentul clasei "ThreadStatic" în ASP.Net este să folosești HttpContext. Așadar, pentru obiectele tale de business, fie continui să folosești if(HttpContext.Current!). =null) și referință System.Web (bleah), sau trebuie să creezi un fel de model de furnizor pentru persistență statică, care trebuie configurat înainte de a accesa aceste singletoane. Greață dublă.
Vă rog să spuneți că nu este cazul.
Anexă: Jurnal complet:
Este o mare bătaie de cap, pentru că, din câte văd eu, singura opțiune de persistență pentru comportamentul 'ThreadStatic' în ASP.Net este să folosești HttpContext. Așadar, pentru obiectele tale de business, fie rămâi blocat cu if(HttpContext.Current!=null) și cu referința System.Web (bleah), fie trebuie să vii cu un model de furnizor pentru persistență statică, care va trebui configurată înainte ca oricare dintre aceste singletoane să fie accesat. Dublu.
Vă rog, cineva să spună că nu e așa.
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(acum)=97, calldata= [3748] INFO 11:10:05,249 ASP. SlowPage_aspx.. ctor() - threadid=3748, threadhash=(cctor)97, threadhash(acum)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(acum)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - Pagina de somn lent....
[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(acum)=1835, calldata= [2720] INFO 11:10:05,679 ASP. FastPage_aspx.. ctor() - threadid=2720, threadhash=(cctor)1835, threadhash(acum)=1835, calldata=2720, logicalcalldata=2720, threadstatic=2720
[3748] INFO 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - Pagina se trezește încet.... [3748] INFO 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(acum)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:06,350 ASP. Global_asax. Application_EndRequest() - threadid=3748, threadhash=97, threadhash(acum)=97, calldata=3748 [3748] INFO 11:10:06,350 ASP. Global_asax. Application_EndRequest() - SFÂRȘIT /SolicităriConcurenteDemo/SlowPage.aspx
[4748] INFO 11:10:06,791 ASP. FastPage_aspx. Page_Load() - threadid=2720, threadhash=(cctor)1835, threadhash(acum)=1703, calldata=, logicalcalldata=, threadstatic= [4748] INFO 11:10:06,791 ASP. Global_asax. Application_EndRequest() - threadid=2720, threadhash=1835, threadhash(acum)=1703, calldata= [4748] INFO 11:10:06,791 ASP. Global_asax. Application_EndRequest() - SFÂRȘIT /SolicităriConcurenteDemo/FastPage.aspx Cheia este ce se întâmplă când Page_Load FastPage este declanșată. ThreadID-ul este 4748, dar ThreadID-ul pe care l-am stocat în HttpContextul ctor este 2720. Codul hash pentru firele logice este 1703, dar codul hash pe care îl stochez în ctor este 1835. Toate datele pe care le-am stocat în CallContext lipsesc (chiar și datele marcate ILogicalThreadAffinnative), dar HttpContext este încă acolo. Așa cum te-ai aștepta, și ThreadStatic-ul meu a dispărut.
Partea cheie este ce se întâmplă când Page_Load FastPage se declanșează. ThreadID-ul este 4748, dar threadID-ul pe care l-am stocat în HttpContext în ctor este 2720. Codul hash pentru firul logic este 1703, dar cel pe care l-am stocat în ctor este 1835. Toate datele pe care le-am stocat în CallContext au dispărut (chiar și cele marcate ILogicalThreadAffinative), dar HttpContext este încă acolo. Așa cum te-ai aștepta, și ThreadStatic-ul meu a dispărut. (Sfârșit)
|