Originál:Prihlásenie na hypertextový odkaz je viditeľné.
Súhrn: Aj keď si myslíte, že viete, čo robíte, nie je bezpečné ukladať čokoľvek do ThreadStatic member, CallContext alebo Thread Local v ASP.Net aplikácii, ak bola hodnota nastavená vopred. Page_Load (napr. v IHttpModule alebo page constructore), ale počas alebo po prístupe.
[Aktualizácia: august 2008 Keďže na tento článok stále odkazuje pomerne veľa ľudí, považujem za potrebné objasniť, že toto správanie pri výmene vlákien nastáva v veľmi konkrétnom bode životného cyklu stránky, a nie vtedy, keď sa to tak cíti.] Moje vyjadrenie po citovaní Jeffa Newsoma je nešťastné. Okrem toho som veľmi rád (a poctený), že sa na tento článok viackrát odkazuje v diskusiách o dizajne o správnom zaobchádzaní s HttpContext. Som rád, že ľudia to považujú za užitočné. ]
Existuje veľa nejasností ohľadom implementácie používateľsky špecifických singletonov v ASP.Net – teda globálne dáta sú globálne len pre jedného používateľa alebo požiadavku. Toto nie je nezvyčajná požiadavka: publikovať transakčné, bezpečnostné kontextové alebo iné "globálne" dáta na jednom mieste, namiesto toho, aby ste ich vkladali do každého volania metódy, pretože loam dáta umožňujú jasnejšiu (a čitateľnejšiu) implementáciu. Ak však nebudete opatrní, je to skvelé miesto, kde si môžete streliť do nohy (alebo hlavy). Myslel som si, že viem, čo sa deje, ale nevedel som.
Preferovanou možnosťou bude singletonUložené v HttpContext.Current.Items, jednoduché a bezpečné, ale spája daný singleton s jeho použitím v ASP.Net aplikácii. Ak singleton zlyhá vo vašom obchodnom objekte, nie je to ideálne. Aj keď zabalíte prístup k vlastnostiam do príkazu if
Summary: Aj keď si myslíte, že viete, čo robíte, nie je bezpečné ukladať čokoľvek do člena ThreadStaticu, CallContext alebo Thread Local Storage v ASP.Net aplikácii, ak je to možné že hodnota môže byť nastavená pred Page_Load (napr. v IHttpModule alebo page constructore), ale prístupná počas alebo po ňom.
[Aktualizácia: august 2008 Vzhľadom na pomerne veľký počet ľudí, ktorí naďalej odkazujú na tento príspevok, cítim potrebu objasniť, že toto správanie pri výmene vlákien sa deje na veľmi konkrétnom mieste na stránke Životný cyklus a nie kedykoľvek, keď sa mu to zachce. Moje vyjadrenie po citáte Jefa Newsona bolo nešťastné. Napriek tomu som bol nesmierne potešený (a poctený) tým, koľkokrát som videl tento príspevok citovaný v diskusiách o správnom zaobchádzaní s HttpContextom. Som rád, že to ľudia považovali za užitočné.]
Existuje veľa zmätku ohľadom implementácie používateľsky špecifických singletonov v ASP.Net – teda globálnych dát, ktoré sú globálne len pre jedného používateľa alebo požiadavku. Nie je to nezvyčajná požiadavka: publikovať transakcie, bezpečnostný kontext alebo iné "globálne" dáta na jednom mieste, namiesto toho, aby sa tieto údaje prenášali cez každé volanie metódy, ako to môže spôsobiť tramp dáta Čistejšia (a čitateľnejšia) implementácia. Je to však skvelé miesto, kde si môžete streliť do nohy (alebo hlavy), ak nie ste opatrní. Myslel som si, že viem, čo sa deje, ale nevedel som.
Preferovanou možnosťou, ukladať singletony do HttpContext.Current.Items, je jednoduchá a bezpečná, ale viaže daný singleton na použitie v ASP.Net aplikácii. Ak je singleton dole vo vašich obchodných objektoch, nie je to ideálne. Aj keď zabalíte prístup k vlastnostiam do príkazu if
... potom stále musíte odkazovať na System.Web z tejto zostavy, ktorá má tendenciu zaradiť viac "webby" objektov na nesprávne miesto.
Alternatívami sú použiť statický člen [ThreadStatic], lokálne úložisko Thread (čo v podstate znamená to isté) alebo CallContext.
Problémy s [ThreadStatic] sú dobre zdokumentované, ale na záver: Field initalizéry sa spustia až pri prvom vlákne Dáta ThreadStatic potrebujú explicitné vyčistenie (napr. v EndRequest), pretože hoci je Thread dostupný, dáta ThreadStatic nebudú v GC, takže môžete unikať zdroje. ThreadStatic dáta sú dobré len v rámci požiadavky, pretože ďalšia požiadavka môže prísť z iného vlákna a získať dáta niekoho iného. Scott Hanselman to vystihuje správne, že ThreadStatic s ASP.Net nefunguje dobre, ale úplne nevysvetľuje prečo.
Úložisko v CallContext niektoré z týchto problémov zmierňuje, pretože kontext na konci požiadavky vyhasne a GC sa nakoniec objaví (hoci stále môžete unikať zdroje, kým GC nenastane, ak skladujete jednorazové veci). Okrem toho je CallContext spôsob, akým sa HttpContext ukladá, takže to musí byť v poriadku, však? Bez ohľadu na to, človek by si myslel (ako ja), že ak po každej žiadosti upraceš, všetko bude v poriadku: "Ak inicializujete premennú ThreadStatic na začiatku požiadavky a správne spracujete referencovaný objekt na konci požiadavky, riskoval by som tvrdenie, že sa nič zlé nestane
"Teraz,Môžem sa mýliť. CLR môže prestať hostovať vlákno v polovici, serializovať jeho zásobník niekde, dať mu nový zásobník a nechať ho začať vykonávať。 Veľmi o tom pochybujem. Predpokladám, že hyperthreading by tiež mohol veci skomplikovať, ale tiež o tom pochybujem. ”
Jeff Newsom
"Ak inicializujete premennú ThreadStatic na začiatku požiadavky a správne odstránite odkazovaný objekt na konci požiadavky, odvážim sa tvrdiť, že nič Stane sa zlé. Si v pohode aj medzi kontextmi v tej istej AppDomain
"Teraz sa môžem mýliť. CLR by mohol potenciálne zastaviť spravované vlákno uprostred streamu, serializovať jeho zásobník niekde, dať mu nový zásobník a nechať ho začať vykonávať. Vážne o tom pochybujem. Predpokladám, že je možné, že hyperthreading veci tiež komplikuje, ale o tom pochybujem." Jef Newsom Aktualizácia: Toto je zavádzajúce. Neskôr podrobnejšie vysvetlím, že táto výmena vlákien môže prebehnúť len medzi BeginRequest a Page_Load, ale Jefova referencia vytvára veľmi silný obraz, ktorý som hneď nedokázal napraviť.
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. Takže v určitom bode ASP.NET rozhodnúť, že je príliš veľa I/O vlákien spracúvajúcich iné požiadavky. […] Prijíma iba požiadavky a zaraďuje ich do interného objektu fronty v rámci ASP.NET runtime. Potom, po zaradení do fronty, I/O vlákno požiada o pracovnú vlákno a následne sa I/O vlákno vráti do svojho poolu. […] Preto ASP.NET nechá túto požiadavku vybaviť tento pracovník. Dostane to do ASP.NET runtime, rovnako ako I/O vlákno pri nízkej záťaži.
Takže v určitom bode ASP.NET rozhodne, že je príliš veľa I/O vlákien spracúvajúcich iné požiadavky. [...] Jednoducho vezme požiadavku a zaradí ju do tohto interného objektu fronty v rámci ASP.NET runtime. Potom, keď je toto vlákno zaradené do fronty, I/O vlákno požiada o worker vlákno a potom sa I/O vlákno vráti do svojho poolu. [...] Takže ASP.NET nechá túto pracovnú niť spracovať požiadavku. Prenesie to do ASP.NET runtime, rovnako ako I/O vlákno pri nízkej záťaži. Vždy som o tom vedel, ale myslím, že sa to stalo veľmi skoro a bolo mi to jedno. Avšak zdá sa, že sa mýlim. Mali sme problém v ASP.Net aplikácii, kde používateľ klikol na odkaz po kliknutí na iný odkaz a naša aplikácia mala v jednom z singletonov výnimku nulového referenčného odkazu (použil som CallContext namiesto ThreadStatic pre singleton, ale ukázalo sa, že to bolo irelevantné).
Urobil som si prieskum o tom, ako presne ASP.Net vlákna fungujú, a dostal som protichodné názory maskované ako fakty (požiadavky sú v požiadavkách agilné podľa vlákien, zatiaľ čo požiadavky sú počas svojej životnosti ukotvené na vláknach), takže som svoj problém skopíroval v testovacej aplikácii s pomalou stránkou (na chvíľu som spal) a rýchlou stránkou. Kliknem na odkaz na pomalú stránku a predtým, než sa stránka vráti, kliknem na odkaz na rýchlu stránku. Výsledok (log4net dump toho, čo sa deje) ma úplne ohromil.
Výstup ukazuje, že pri druhej požiadavke sa udalosť BeginRequest a konštruktor stránok v pipeline HttpModule spustia na jednom vlákne, ale page_Load na inom. Druhé vlákno už migrovalo HttpContext z prvého vlákna, ale nie CallContext ani ThreadStatic (poznámka: keďže samotný HttpContext je uložený v CallContext, znamená to, že ASP.Net explicitne migruje HttpContext). Povedzme to znova:
Vždy som o tom vedel, ale predpokladal som, že sa to stalo dosť skoro na to, aby mi to bolo jedno. Zdá sa však, že som sa mýlil. Máme problém v našej ASP.Net aplikácii, kde používateľ klikne na jeden odkaz hneď po tom, čo klikne na iný, a naša aplikácia vybuchne s nulovou referenčnou výnimkou pre jeden z našich singletonov (používam CallContext, nie ThreadStatic pre singleton, ale ukazuje sa, že na tom nezáleží).
Trochu som si naštudoval, ako presne ASP. Netovo vlákno funguje a má protichodné názory – predstierajúce, že sú fakty (požiadavky sú agilné v rámci požiadavky oproti požiadavkám pripnuté k vláknu na celý život), tak som replikoval svoje Problém v testovacej aplikácii s pomalou stránkou (spanie na sekundu) a rýchlou stránkou. Kliknem na odkaz na pomalú stránku a predtým, než sa stránka vráti, kliknem na odkaz na rýchlu stránku. Výsledky (log4net výpis toho, čo sa deje) ma prekvapili.
Výstup ukazuje, že pri druhom požiadavke sa udalosti BeginRequest v pipeline HttpModule a konštruktor stránok spustia na jednom vlákne, ale Page_Load na inom. Druhé vlákno malo HttpContext migrovaný z prvého, ale nie CallContext ani ThreadStatic (Poznámka: keďže HttpContext je sám uložený v CallContext, znamená to, že ASP.Net je explicitne migrujúc HttpContext naprieč). Poďme si to ešte raz vysvetliť:
- Prepínanie vlákien nastáva po vytvorení IHttpHandlera
- Po spustení inicializátora poľa a konštruktora stránky
- Po akýchkoľvek udalostiach typu BeginRequest, AuthenticateRequest, AquireSessionState, ktoré používajú Global.ASA/IHttpModules.
- Iba HttpContext sa migruje do nového vlákna
Prepínanie vlákien nastáva po vytvorení IHttpHandlera Po spustení poľa a konštruktora stránky Po akýchkoľvek udalostiach typu BeginRequest, AuthenticateRequest, AquireSessionState, ktoré používajú vaše Global.ASA / IHttpModule. Iba HttpContext migruje do nového vlákna Je to veľká otrava, pretože podľa toho, čo som videl, to znamená, že jedinou možnosťou perzistencie správania triedy "ThreadStatic" v ASP.Net je použiť HttpContext. Takže pre vaše obchodné objekty buď pokračujete v používaní if(HttpContext.Current!). =null) a referenciou na System.Web (fuj), alebo musíte vytvoriť nejaký model poskytovateľa pre statickú perzistenciu, ktorý je potrebné nastaviť pred prístupom k týmto singletonom. Dvojitá nevoľnosť.
Prosím, povedzte, že to tak nie je.
Príloha: Celý denník:
To je veľká otrava, pretože podľa mňa to znamená, že jedinou možnosťou perzistencie pre správanie podobné 'ThreadStatic' v ASP.Net je použiť HttpContext. Takže pre vaše obchodné objekty buď ste uviaznutí pri if(HttpContext.Current!=null) a referencii na System.Web (fuj), alebo musíte prísť s nejakým modelom poskytovateľa Statická perzistencia, ktorú bude potrebné nastaviť ešte predtým, než sa k nim pristupuje. Dvojité fuj.
Prosím, niekto povedzte, že to tak nie je.
Appendix: That log in full: [3748] INFO 11:10:05,239 ASP. Global_asax. Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/SlowPage.aspx [3748] INFO 11:10:05,239 ASP. Global_asax. Application_BeginRequest() - threadid=, threadhash=, threadhash(teraz)=97, calldata= [3748] INFO 11:10:05,249 ASP. SlowPage_aspx.. ctor() - threadid=3748, threadhash=(cctor)97, threadhash(teraz)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(teraz)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - Pomalý spánok stránky....
[2720] INFO 11:10:05,669 ASP. Global_asax. Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/FastPage.aspx [2720] INFO 11:10:05,679 ASP. Global_asax. Application_BeginRequest() - threadid=, threadhash=, threadhash(teraz)=1835, calldata= [2720] INFO 11:10:05,679 ASP. FastPage_aspx.. ctor() - threadid=2720, threadhash=(cctor)1835, threadhash(teraz)=1835, calldata=2720, logicalcalldata=2720, threadstatic=2720
[3748] INFO 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - Pomalé prebúdzanie stránky.... [3748] INFO 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(teraz)=97, calldata=3748, logicalcalldata=3748 [3748] INFO 11:10:06,350 ASP. Global_asax. Application_EndRequest() - threadid=3748, threadhash=97, threadhash(teraz)=97, calldata=3748 [3748] INFO 11:10:06,350 ASP. Global_asax. Application_EndRequest() - KONIEC /ConcurrentRequestsDemo/SlowPage.aspx
[4748] INFO 11:10:06,791 ASP. FastPage_aspx. Page_Load() - threadid=2720, threadhash=(cctor)1835, threadhash(teraz)=1703, calldata=, logicalcalldata=, threadstatic= [4748] INFO 11:10:06,791 ASP. Global_asax. Application_EndRequest() - threadid=2720, threadhash=1835, threadhash(teraz)=1703, calldata= [4748] INFO 11:10:06,791 ASP. Global_asax. Application_EndRequest() - KONIEC /ConcurrentRequestsDemo/FastPage.aspx Kľúčové je, čo sa stane, keď sa spustí Page_Load FastPage. ThreadID je 4748, ale ThreadID, ktoré som uložil do HttpContext ctor, je 2720. Hash kód pre logické vlákna je 1703, ale hash kód, ktorý ukladám do ctor je 1835. Všetky dáta, ktoré som uložil do CallContext, chýbajú (dokonca aj údaje označené ILogicalThreadAffinnative), ale HttpContext tam stále je. Ako by ste čakali, aj môj ThreadStatic je preč.
Kľúčové je, čo sa stane, keď sa spustí Page_Load FastPage. ThreadID je 4748, ale threadID, ktoré som uložil do HttpContext v ctor, je 2720. Hash kód logického vlákna je 1703, ale ten, ktorý som uložil do ktoru, je 1835. Všetky dáta, ktoré som uložil do CallContext, sú preč (aj tie označené ako ILogicalThreadAffinative), ale HttpContext tam stále je. Ako by ste čakali, aj môj ThreadStatic je preč. (Koniec)
|