Оригинален:Входът към хиперлинк е видим.
Резюме: Дори и да мислите, че знаете какво правите, не е безопасно да съхранявате нещо в ThreadStatic member, CallContext или Thread Local в ASP.Net приложение, ако стойността е била зададена предварително. да Page_Load (например в IHttpModule или конструктор на страници), но по време или след достъпа.
[Актуализация: август 2008 г. Като се има предвид, че доста хора продължават да линкват към тази статия, смятам, че е необходимо да уточня, че това поведение при смяна на теми се случва в много специфичен момент от жизнения цикъл на страницата, а не когато му се струва.] Формулировката ми след цитирането на Джеф Нюсъм е нещастна. Освен това съм много доволен (и поластен) да видя многократни препратки към тази статия в дизайнерските дискусии за правилното управление на HttpContext. Радвам се, че хората го намират за полезен. ]
Има много объркване относно това как да се имплементират потребителски специфични сингълтони в ASP.Net – тоест глобалните данни са глобални само за един потребител или заявка. Това не е необичайно изискване: публикувайте транзакционни, контекстни за сигурност или други "глобални" данни на едно място, вместо да ги прилагате във всяко извикване на метод, защото loam данните позволяват по-ясна (и по-четлива) реализация. Въпреки това, ако не сте внимателни, това е чудесно място да си нараните крака (или главата). Мислех, че знам какво се случва, но не знаех.
Предпочитаният вариант ще бъде singletonСъхранявани в HttpContext.Current.Items, прости и сигурни, но свързвайки въпросния Singleton с използването му в приложението ASP.Net. Ако singleton не изпълнява бизнес целта, това не е идеално. Дори ако обвиете достъпа до имота в if изявление
Summary: Дори и да мислите, че знаете какво правите, не е безопасно да съхранявате нещо в ThreadStatic member, CallContext или Thread Local Storage в ASP.Net приложение, ако има такава възможност че стойността може да бъде зададена преди Page_Load (например в IHttpModule или конструктор на страници), но достъпена по време или след това.
[Актуализация: август 2008 С оглед на доста големия брой хора, които продължават да линкват към този пост, чувствам нужда да уточня, че това поведение при смяна на нишки се случва в много специфичен момент на страницата Жизнен цикъл, а не когато ти се струва така. Формулировката ми след цитата на Джеф Нюсън беше нещастна. Отделяйки това настрана, бях изключително удовлетворен (и поластен) от броя пъти, в които съм виждал този пост цитиран в дизайнерските дискусии относно правилното отношение към HttpContext. Радвам се, че хората го намериха за полезен.]
Има много объркване относно използването на потребителски специфични сингълтони в ASP.Net – тоест глобални данни, които са глобални само за един потребител или заявка. Това не е необичайно изискване: публикуване на транзакции, контекст за сигурност или други "глобални" данни на едно място, вместо да се изпращат през всяко извикване на метод, както tramp data може да доведе до по-чиста (и по-четлива) реализация. Въпреки това, това е чудесно място да си нараниш крака (или главата), ако не си внимателен. Мислех, че знам какво се случва, но не знаех.
Предпочитаният вариант – съхраняването на сингълтоните в HttpContext.Current.Items – е прост и безопасен, но свързва въпросния сингълтон с използването му в ASP.Net приложение. Ако единичният е в бизнес обектите ви, това не е идеално. Дори ако обвиете достъпа до имота в if оператор
... след това все пак трябва да се позоваваш на System.Web от този асемблер, който обикновено включва повече 'webby' обекти на грешното място.
Алтернативите са да се използва статичен член [ThreadStatic], локално съхранение на Thread (което по същество е същото) или CallContext.
Проблемите с [ThreadStatic] са добре документирани, но за да обобщим: Полевите инициатори стрелят само при първата нишка ThreadStatic данните трябва да бъдат изрично почистени (например в EndRequest), защото докато Thread е достъпен, ThreadStatic данните няма да бъдат GC, така че може да изтичате ресурси. ThreadStatic данни са полезни само в рамките на заявка, защото следващата заявка може да дойде в друга нишка и да получи чужди данни. Скот Ханселман е прав, че ThreadStatic не се вписва добре в ASP.Net, но не обяснява напълно защо.
Съхранението в CallContext облекчава някои от тези проблеми, тъй като контекстът изчезва в края на заявката и GC ще се появи в крайна сметка (въпреки че все още можете да изтичате ресурси, докато GC се случи, ако съхраняваш еднократни предмети). Освен това CallContext е начинът, по който се съхранява HttpContext, така че трябва да е окей, нали? Независимо от това, човек би си помислил (както и аз), че ако си почистиш след себе си в края на всяка молба, всичко ще е наред: "Ако инициализирате променливата ThreadStatic в началото на заявката и правилно обработите реферирания обект в края на заявката, бих рискувал да твърдя, че нищо лошо няма да се случи
"Сега,Може и да греша. CLR може да спре да хоства нишка по средата, да сериализира стека си някъде, да му даде нов стек и да го остави да започне да се изпълнява。 Дълбоко се съмнявам в това. Предполагам, че е възможно и хипертредингът да затрудни нещата, но също така се съмнявам. ”
Джеф Нюсъм
"Ако инициализираш променлива ThreadStatic в началото на заявката и правилно се отървеш от реферирания обект в края на заявката, ще рискувам и ще твърдя, че нищо Лошо ще се случи. Дори си готин между контекстите в един и същи AppDomain
"Може и да греша. CLR потенциално може да спре управлявана нишка по средата на потока, да сериализира нейния стек някъде, да му даде нов стек и да го остави да започне изпълнение. Сериозно се съмнявам. Предполагам, че е възможно и хипертрейдингът да затрудни нещата, но също така се съмнявам в това." Джеф Нюсъм Актуализация: Това е подвеждащо. Ще обясня по-нататък, че тази смяна на тема може да се случи само между BeginRequest и Page_Load, но препратката на Джеф създава много силно изображение, което не успях да коригирам веднага.
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. Така че, в даден момент ASP.NET реши, че има твърде много I/O нишки, които обработват други заявки. […] Приема заявки само и ги поставя на опашка в обект за вътрешна опашка в рамките ASP.NET време на изпълнение. След това, след опашката, I/O нишката ще поиска работна нишка, а след това I/O нишката ще се върне в своя пул. […] Затова ASP.NET оставям този работник да обработи заявката. Ще го доведе до ASP.NET runtime, точно както I/O нишката при ниско натоварване.
Така че в един момент ASP.NET реши, че има твърде много I/O нишки, които обработват други заявки. [...] Просто взима заявката и я поставя на опашка в този вътрешен обект на опашка в рамките на ASP.NET runtime. След това, след като това е поставено на опашка, I/O нишката ще поиска работна нишка и след това I/O нишката ще бъде върната в своя пул. [...] Затова ASP.NET ще накарам работната нида да обработи заявката. Ще го отведе в ASP.NET runtime, точно както I/O нишката би го направила при ниско натоварване. Винаги съм знаел за това, но мисля, че се случи много рано и не ми пукаше. Въпреки това, изглежда, че греша. Имахме проблем в ASP.Net приложение, където потребител кликна на линк след като е кликнал на друг линк, и нашето приложение имаше изключение null reference в един от singleton-ите (използвах CallContext вместо ThreadStatic за singleton, но се оказа, че е без значение).
Направих проучване как точно работят ASP.Net нишки и получих противоречиви мнения, маскирани като факти (заявките са гъвкави по нишки в заявките, докато заявките са закрепени върху нишки през целия им живот), затова копирах проблема си в тестово приложение с бавна страница (за секунда в сън) и бърза страница. Кликвам върху линка към бавната страница и преди страницата да се върне, кликвам на линка към бързата страница. Резултатът (log4net dump на случващото се) ме порази.
Изходът показва, че за втората заявка събитието BeginRequest и конструкторът на страница в HttpModule pipeline се активират в една нишка, но page_Load в друга. Втората нишка вече е мигрирала HttpContext от първата нишка, но не и CallContext или ThreadStatic (забележка: тъй като самият HttpContext се съхранява в CallContext, това означава, че ASP.Net изрично мигрира HttpContext). Нека го кажем отново:
Винаги съм знаел за това, но предполагах, че се е случило достатъчно рано в процеса, за да не ми пука. Изглежда обаче, че съм грешал. Имаме проблем в нашето ASP.Net приложение, при който потребителят кликва върху един линк веднага след друг, а приложението ни се разпространява с изключение за null reference за един от нашите singletons (аз използвам CallContext, не ThreadStatic за singleton, но се оказва, че няма значение).
Направих малко проучване за това как точно е ASP. Threading на Net работи и получи противоречиви мнения – маскирани като факти (заявките са thread-agile в рамките на заявка, докато заявките са закрепени към нишка за целия им живот), затова репликрах моя Проблем в тестово приложение с бавна страница (заспива за секунда) и бърза страница. Кликвам на линка към бавната страница и преди страницата да се върне, натискам линка към бързата страница. Резултатите (log4net dump на случващото се) ме изненада.
Резултатът показва, че – за втората заявка – събитията BeginRequest в HttpModule pipeline и конструкторът на страници се задействат в една нишка, но Page_Load се активира на друга. Втората нишка е имала миграция на HttpContext от първата, но не и от CallContext или ThreadStatic (забележка: тъй като самият HttpContext се съхранява в CallContext, това означава, че ASP.Net е изрично мигриране на HttpContext настрани). Нека го обясним отново:
- Превключването на нишки се случва след създаването на IHttpHandler
- След инициализатора на полето и конструктора на страницата
- След всяко събитие от типа BeginRequest, AuthenticateRequest, AquireSessionState, които се използват от Global.ASA/IHttpModules.
- Само HttpContext се мигрира към новата нишка
Превключването на нишките се случва след като IHttpHandler е създаден След инициализацията на полетата на страницата и конструкторите се стартират След всяко събитие от типа StartRequest, AuthenticateRequest, AquireSessionState, които вашите Global.ASA / IHttpModules използват. Само HttpContext мигрира към новата нишка Това е голямо досадно, защото от това, което съм виждал, означава, че единствената опция за устойчивост на поведението на класа "ThreadStatic" в ASP.Net е използването на HttpContext. Така че за бизнес обектите ти или продължаваш да използваш if(HttpContext.Current!). =null) и System.Web reference (ужас), или трябва да измислите някакъв модел на доставчик за статична персистентност, който трябва да се настрои преди достъп до тези сингълтони. Двойно гадене.
Моля, кажете, че това не е така.
Приложение: Пълен дневник:
Това е голяма досада, защото доколкото виждам, единствената опция за устойчивост за поведение в стил "ThreadStatic" в ASP.Net е използването на HttpContext. Така че за вашите бизнес обекти или сте ограничени с if(HttpContext.Current!=null) и System.Web reference (ужас), или трябва да измислите някакъв модел на доставчик за вашия статична устойчивост, която трябва да се настрои преди да се достъпи някой от тези единични устройства. Двойно отвратително.
Моля, някой да каже, че не е така.
Appendix: That log in full: [3748] ИНФО 11:10:05,239 ASP. Global_asax. Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/SlowPage.aspx [3748] ИНФО 11:10:05,239 ASP. Global_asax. Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=97, calldata= [3748] ИНФО 11:10:05,249 ASP. SlowPage_aspx.. ctor() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] ИНФОРМАЦИЯ 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] ИНФОРМАЦИЯ 11:10:05,349 ASP. SlowPage_aspx. Page_Load() - Бавно заспиване на страници....
[2720] ИНФО 11:10:05,669 ASP. Global_asax. Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/FastPage.aspx [2720] ИНФО 11:10:05,679 ASP. Global_asax. Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=1835, calldata= [2720] ИНФО 11:10:05,679 ASP. FastPage_aspx.. ctor() - threadid=2720, threadhash=(cctor)1835, threadhash(now)=1835, calldata=2720, logicalcalldata=2720, threadstatic=2720
[3748] ИНФОРМАЦИЯ 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - Бавно пробуждане на страницата.... [3748] ИНФОРМАЦИЯ 11:10:06,350 ASP. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] ИНФОРМАЦИЯ 11:10:06,350 ASP. Global_asax. Application_EndRequest() - threadid=3748, threadhash=97, threadhash(now)=97, calldata=3748 [3748] ИНФОРМАЦИЯ 11:10:06,350 ASP. Global_asax. Application_EndRequest() - КРАЙ /ConcurrentRequestsDemo/SlowPage.aspx
[4748] ИНФОРМАЦИЯ 11:10:06,791 ASP. FastPage_aspx. Page_Load() - threadid=2720, threadhash=(cctor)1835, threadhash(now)=1703, calldata=, logicalcalldata=, threadstatic= [4748] ИНФОРМАЦИЯ 11:10:06,791 ASP. Global_asax. Application_EndRequest() - threadid=2720, threadhash=1835, threadhash(now)=1703, calldata= [4748] ИНФОРМАЦИЯ 11:10:06,791 ASP. Global_asax. Application_EndRequest() - КРАЙ /ConcurrentRequestsDemo/FastPage.aspx Ключът е какво се случва, когато се задейства Page_Load на FastPage. ThreadID е 4748, но ThreadID, който съхраних в HttpContext на ctor, е 2720. Хеш кодът за логически нишки е 1703, но хеш кодът, който съхранявам в CTOR, е 1835. Всички данни, които съхраних в CallContext, липсват (дори данните, маркирани като ILogicalThreadAffinnative), но HttpContext все още е там. Както може да се очаква, ThreadStatic също го няма.
Ключовото е какво се случва, когато Page_Load на FastPage се задейства. ThreadID е 4748, но ThreadID, който съхраних в HttpContext в CTOR-а, е 2720. Хеш кодът за логическата нишка е 1703, но този, който съхраних в CTOR, е 1835. Всички данни, които съхраних в CallContext, са изчезнали (дори този, който е маркиран като ILogicalThreadAffinative), но HttpContext все още е там. Както се очаква, и моят ThreadStatic го няма. (Край)
|