원문 언어:하이퍼링크 로그인이 보입니다.
요약: 무엇을 하는지 안다고 생각해도, 값이 미리 설정되어 있을 경우 ASP.Net 애플리케이션의 ThreadStatic 멤버, CallContext, 또는 Thread Local 저장소에 저장하는 것은 안전하지 않습니다. Page_Load(예: IHttpModule 또는 페이지 생성자에서)에 접근할 수 있지만, 접근 중이나 접근 후에 사용할 수 있습니다.
[업데이트: 2008년 8월 이 글에 계속 링크하는 분들이 꽤 많으니, 이 스레드 스왑 동작은 페이지 수명 주기의 매우 특정 시점에 발생하며, 언제가 그렇게 느껴지는 것이 아님을 명확히 할 필요가 있다고 생각합니다.] 제프 뉴스옴을 인용한 후 쓴 표현은 유감스럽습니다. 그 외에는, HttpContext의 올바른 처리에 관한 디자인 논의에서 이 글이 여러 번 언급되는 것을 보니 매우 기쁘고 영광스럽습니다. 사람들이 유용하다고 생각한다니 기쁩니다. ]
ASP.Net 에서 사용자 전용 싱글턴을 어떻게 구현하는지에 대해 많은 혼란이 있습니다. 즉, 글로벌 데이터는 한 명의 사용자나 요청에만 대해 전역적으로 할당됩니다. 이는 드문 요구사항이 아닙니다: 트랜잭션, 보안 컨텍스트 또는 기타 "전역" 데이터를 한 곳에 게시하는 것이 모든 메서드 호출에 밀어붙이기보다는 로암 데이터가 더 명확하고 읽기 쉬운 구현을 가능하게 하기 때문입니다. 하지만 조심하지 않으면 스스로 발등을 찍을 수 있는 좋은 곳입니다(또는 머리). 무슨 일이 일어나고 있는지 안다고 생각했지만, 사실은 몰랐습니다.
선호되는 옵션은 싱글턴일 것입니다HttpContext.Current.Items에 저장되어 간단하고 안전합니다그러나 문제의 싱글톤을 ASP.Net 적용에서의 사용과 연결시킨다. 비즈니스 객체에서 싱글턴이 실패하면 이상적이지 않습니다. 속성 접근을 if 문으로 감싸더라도
Summary: 무엇을 하는지 안다고 생각해도, ASP.Net 애플리케이션 내 ThreadStatic 멤버, CallContext, 또는 Thread 로컬 저장소에 아무것도 저장하는 것은 안전하지 않습니다. 값이 Page_Load 이전에 설정될 수 있다는 점(예: IHttpModule이나 페이지 생성자에서)이나, 중이나 그 후에 접근할 수 있다는 점입니다.
[업데이트: 2008년 8월 이 게시물에 계속 링크하는 사람이 꽤 많다는 점을 고려할 때, 이 스레드 스왑 행위는 페이지의 매우 특정 지점에서 발생한다는 점을 명확히 할 필요가 있다고 느낍니다 생애 주기를 말이지, 기분 내킬 때가 아니라. 제프 뉴스슨 인용문 이후의 제 표현은 유감스러웠습니다. 그건 제쳐두고, HttpContext를 적절히 다루는 디자인 논의에서 이 글이 여러 번 인용되는 것을 보고 매우 만족스럽고 영광스럽게 느꼈습니다. 사람들이 유용하다고 느꼈다니 기쁩니다.]
ASP.Net 에서 사용자 전용 싱글턴을 구현하는 방법에 대해 혼란이 많습니다. 즉, 한 사용자나 요청에만 전역적으로 적용되는 글로벌 데이터를 의미합니다. 이는 드문 요구사항이 아닙니다: 트랜잭션, 보안 컨텍스트 또는 기타 '전역적인' 데이터를 한 곳에 게시하는 것이, 트램프 데이터처럼 모든 메서드 호출을 통해 밀어붙이는 대신 더 깔끔하고 더 읽기 쉬운 구현입니다. 하지만 조심하지 않으면 발목(또는 머리)을 찍을 수 있는 좋은 장소입니다. 무슨 일이 일어나고 있는지 안다고 생각했지만, 사실은 몰랐습니다.
선호되는 옵션은 HttpContext.Current.Items에 싱글톤을 저장하는 것으로, 간단하고 안전하지만, 해당 싱글톤을 ASP.Net 애플리케이션 내에서 사용하도록 묶어둡니다. 사업 대상에 싱글 인원이 있다면 이상적이지 않습니다. property-access를 if 문으로 감싸도
... 그럼에도 불구하고 그 어셈블리에서 System.Web을 참조해야 하는데, 이 방법은 잘못된 위치에 더 많은 'webby' 객체를 인코레이징하는 경향이 있습니다.
대안으로는 [ThreadStatic] 정적 멤버, Thread 로컬 저장소(거의 같은 목적), 또는 CallContext를 사용하는 방법이 있습니다.
[ThreadStatic]의 문제들은 잘 문서화되어 있지만, 요약하자면: 필드 이니탈라이저는 첫 번째 스레드에서만 발화됩니다 ThreadStatic 데이터는 명시적으로 정리가 필요합니다(예: EndRequest에서). 스레드는 접근 가능하지만 ThreadStatic 데이터는 GC되지 않아 자원이 누설될 수 있습니다. ThreadStatic 데이터는 요청 내에서만 유효한데, 다음 요청이 다른 스레드에서 들어올 수 있고 다른 사람의 데이터를 가져갈 수도 있기 때문입니다. 스콧 한셀먼은 ThreadStatic이 ASP.Net 와 잘 어울리지 않는다는 점을 정확히 지적했지만, 그 이유를 완전히 설명하지는 않습니다.
CallContext의 스토리지는 이러한 문제를 일부 완화해줍니다. 요청이 끝나면 컨텍스트가 사라지고 결국 GC가 발생하기 때문입니다(하지만 GC가 발생할 때까지 리소스를 유출할 수는 있습니다). 일회용품을 보관하는 거죠). 게다가 CallContext가 HttpContext를 저장하는 방식이니 괜찮을 거예요, 맞죠? 상관없이, 저도 그렇게 생각했지만, 각 요청이 끝날 때마다 정리만 하면 모든 게 괜찮을 거라고 생각할 수 있겠네요: "요청 시작 시 ThreadStatic 변수를 초기화하고 요청 끝에 참조된 객체를 올바르게 처리한다면, 아무 일도 일어나지 않을 거라고 주장할 위험을 감수할 수 있습니다
"이제,틀릴 수도 있어요. CLR은 스레드를 중간에 호스팅을 중단하고, 그 스택을 어딘가에 직렬화한 뒤 새 스택을 주고 실행을 시작할 수 있습니다。 저는 이 점에 대해 깊이 의심합니다. 하이퍼스레딩이 문제를 복잡하게 만들 수도 있겠지만, 저는 그럴 가능성도 낮다고 생각합니다. ”
제프 뉴스옴
"요청 시작 시 ThreadStatic 변수를 초기화하고, 요청 끝에 참조된 객체를 적절히 폐기한다면, 저는 감히 아무것도 없다고 주장하겠습니다 나쁜 일이 일어날 거야. 같은 AppDomain 내에서 여러 컨텍스트 사이에서도 멋지게 지내실 수 있습니다
"이제, 내가 틀릴 수도 있어. clr은 관리 스레드를 스트림 중간에 멈추고, 그 스택을 어딘가에서 직렬화한 뒤 새 스택을 주고 실행을 시작할 수 있습니다. 저는 정말 그럴 리 없다고 생각합니다. 하이퍼스레딩이 문제를 일으킬 가능성도 있지만, 저는 그럴 가능성도 낮다고 봅니다." 제프 뉴스옴 업데이트: 이 내용은 오해의 소지가 있습니다. 이 스레드 교체는 BeginRequest와 Page_Load 사이에서만 가능하다는 점을 나중에 더 설명하겠지만, Jef의 참조가 매우 강력한 이미지를 만들어내서 바로 수정하지 못했습니다.
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 런타임으로 전환되는데, 이는 저로드 상태에서 I/O 스레드가 작동하는 것과 같습니다.
그래서 어느 시점에 ASP.NET 다른 요청을 처리하는 I/O 스레드가 너무 많다고 판단하게 됩니다. [...] 요청을 받아 ASP.NET 런타임 내에 이 내부 큐 객체에 큐에 넣습니다. 그 후 대기열에 들어가면 I/O 스레드가 워커 스레드를 요청하고, I/O 스레드는 풀로 반환됩니다. [...] 그래서 ASP.NET 그 워커 스레드가 요청을 처리하게 됩니다. 이로 인해 ASP.NET 런타임으로 전환되는데, 이는 저부중 상태에서 I/O 스레드가 그랬던 것과 같습니다. 사실 저는 항상 알고 있었지만, 아주 일찍 일어난 일이었고 신경 쓰지 않았어요. 하지만 제가 틀린 것 같습니다. 저희 앱 ASP.Net 에서 사용자가 다른 링크를 클릭한 후에 링크를 클릭한 문제가 있었는데, 우리 앱의 싱글톤 중 하나에 null reference 예외가 있었습니다(싱글톤에는 ThreadStatic 대신 CallContext를 사용했지만 결과는 무관했습니다).
스레드가 정 ASP.Net 확히 어떻게 작동하는지 조사해봤는데, 사실인 척하는 상반된 의견들이 나왔습니다(요청은 요청에서 스레드 애자일이지만, 요청은 스레드 수명 동안 스레드에 고정됩니다). 그래서 느린 페이지(잠시 잠들었음)와 빠른 페이지가 있는 테스트 애플리케이션에 제 문제를 복사했습니다. 느린 페이지 링크를 클릭한 뒤, 페이지가 돌아오기 전에 빠른 페이지 링크를 클릭합니다. 결과물(로그4net에 일어나는 일을 덤프한 것)는 정말 충격적이었습니다.
출력은 두 번째 요청에서 HttpModule 파이프라인의 BeginRequest 이벤트와 페이지 생성자가 한 스레드에서 실행되지만 다른 스레드에서는 page_Load 있음을 보여줍니다. 두 번째 스레드는 이미 첫 번째 스레드에서 HttpContext를 마이그레이션했지만, CallContext나 ThreadStatic은 마이그레이션하지 않았습니다(참고: HttpContext 자체가 CallContext에 저장되므로 ASP.Net 가 명시적으로 HttpContext를 마이그레이션하고 있음을 의미합니다). 다시 말해보자:
사실 저는 항상 이 사실을 알고 있었지만, 과정 초기에 일어난 일이라 신경 쓰지 않았어요. 하지만 제가 틀렸던 것 같습니다. 저희 ASP.Net 앱에서 사용자가 한 링크를 클릭한 직후에 다른 링크를 클릭하는데, 저희 싱글톤 중 하나에 대해 null reference 예외가 발생하는 문제가 발생하고 있습니다 싱글톤에서는 CallContext가 아닌 ThreadStatic을 사용하지만, 사실 상관없다는 사실이 밝혀졌습니다).
ASP가 정확히 어떻게 작동하는지 조금 조사해봤습니다. Net의 스레딩은 잘 작동했고, 요청이 스레드 민첩성이 요청 내에서 스레드 민첩성을 갖는 것과 요청이 한 스레드에 고정되어 생애 동안 고정되는 의견이 상반되어 저는 테스트 애플리케이션에서 느린 페이지(잠시 잠들어 있음)와 빠른 페이지가 있는 문제입니다. 느린 페이지 링크를 클릭한 뒤, 페이지가 다시 오기 전에 빠른 페이지 링크를 클릭했어요. 결과(로그4넷에 일어나는 일을 정리한 것)는 저를 놀라게 했습니다.
출력에 따르면 두 번째 요청에서는 HttpModule 파이프라인의 BeginRequest 이벤트와 페이지 생성자는 한 스레드에서 실행되지만, Page_Load는 다른 스레드에서 실행됩니다. 두 번째 스레드는 HttpContext가 첫 번째에서 이전되었지만, CallContext나 ThreadStatic은 이전되지 않았습니다(참고: HttpContext 자체가 CallContext에 저장되므로 ASP.Net HttpContext를 명시적으로 마이그레이션하는 것). 다시 한 번 명확히 해보자:
- 스레드 전환은 IHttpHandler가 생성된 후에 발생합니다
- 페이지의 필드 초기화자와 생성자 실행 후
- Global.ASA/IHttpModules에서 사용되는 BeginRequest, AuthenticateRequest, AquireSessionState 유형 이벤트 이후에도요.
- 오직 HttpContext만 새 스레드로 이전됩니다
스레드 전환은 IHttpHandler가 생성된 후에 발생합니다 페이지의 필드 초기화와 생성자 실행 후 Global.ASA / IHttpModules 사용 중인 BeginRequest, AuthenticateRequest, AquireSessionState 유형 이벤트가 발생한 후에도요. 오직 HttpContext만이 새 스레드로 이전합니다 이건 정말 골치 아픈데, 제가 본 바로는 ASP.Net 에서 "ThreadStatic" 클래스의 동작을 영지화하는 유일한 방법이 HttpContext를 사용하는 것뿐이라는 의미입니다. 비즈니스 객체의 경우, if(HttpContext.Current!)를 계속 사용하세요. =null) 그리고 System.Web 참조(역겨움)를 하거나, 정적 영속성을 위한 제공자 모델을 만들어야 하는데, 이 모델들은 싱글톤에 접근하기 전에 설정해야 합니다. 메스꺼움이 두 배로 심해졌어요.
제발 그렇지 않다고 말해 주세요.
부록: 전체 로그:
이건 정말 번거로운 문제인데, 제가 보기에는 ASP.Net 에서 'ThreadStatic' 같은 동작을 할 수 있는 유일한 영구 옵션은 HttpContext를 사용하는 것뿐인 것 같습니다. 따라서 비즈니스 객체의 경우, if(HttpContext.Current!=null)와 System.Web 참조(별로)를 사용하거나, 아니면 어떤 형태로든 제공자 모델을 만들어야 합니다 정적 지속성(static persistence)은 이 싱글톤 중 하나에 접근하기 전에 설정해야 합니다. 두 배로 역겹다.
제발 누군가 그게 아니라고 말해줘.
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 시속 시간. SlowPage_aspx. Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] 정보 11:10:05,349 시속 시간. 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 핵심은 FastPage의 Page_Load가 발동될 때 어떤 일이 벌어지는가입니다. ThreadID는 4748이지만, ctor의 HttpContext에 저장한 ThreadID는 2720입니다. 논리 스레드의 해시 코드는 1703이지만, ctor에 저장하는 해시 코드는 1835입니다. CallContext에 저장한 모든 데이터가 사라졌고(ILogicalThreadAffinnative로 표시된 데이터도 포함), HttpContext는 여전히 있습니다. 예상하셨겠지만, 제 ThreadStatic도 사라졌습니다.
핵심은 FastPage의 Page_Load가 발사될 때 무슨 일이 일어나는가입니다. ThreadID는 4748이지만, ctor의 HttpContext에 저장된 threadID는 2720입니다. 논리 스레드의 해시 코드는 1703이지만, ctor에 저장한 해시 코드는 1835입니다. CallContext에 저장한 모든 데이터가 사라졌고(ILogicalThreadAffinative라고 표시된 데이터도 포함해서), HttpContext는 여전히 남아 있습니다. 예상했겠지만, 제 ThreadStatic도 사라졌습니다. (끝)
|