Microsoft MVP성태의 닷넷 이야기
.NET Framework: 89. ManagedThreadId - 두 번째 이야기 [링크 복사], [링크+제목 복사]
조회: 19080
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 2개 있습니다.)
.NET Framework: 88. ManagedThreadId ?
; https://www.sysnet.pe.kr/2/0/491

.NET Framework: 89. ManagedThreadId - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/492





ManagedThreadId - 두 번째 이야기


[이 토픽은 이전 "ManagedThreadId ?" 글에 달린 댓글에 대해 답변으로 쓴 것입니다.]

물론, ManagedThreadId가 Thread.GetHashCode의 래퍼에 불과하긴 하지만. 관리 스레드와 Native 스레드를 구분해야 하는 이유가 Fiber의 도입 여부에 의존한다는 것은 간과할 수 없는 사실로 보입니다. 실제로 Obsolete에 나온 경고문이나, MSDN 도움말에 나온 ManagedThreadId의 설명을 보아도 그렇습니다.

.NET Framework Class Library  - Thread.ManagedThreadId Property  
; https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.managedthreadid

The value of the ManagedThreadId property does not vary over time, 
even if unmanaged code that hosts the common language runtime implements the thread as a fiber.

말씀하신 내용 중에,

"
굳이 Fiber를 운운하지 않더라도 호스팅 환경에 따라 서로 다른 두 개 이상의 운영체제 스레드가 하나의 Thread 객체에 매핑될 수 있기 때문입니다.
"



위와 같이 언급하신 것이 있는데, 제 생각에는 .NET 1.x의 그 문서 내용을 너무 확장해서 판단하신 것이 아닌가 생각됩니다. 현실적으로 볼 때 "서로 다른 두 개 이상의 운영체제 스레드가 하나의 .NET Thread 객체에 매핑"되는 것은 불가능하기 때문입니다. 만약, 그러한 매핑이 가능하다면 (ThreadStatic 특성이 적용된 것을 포함해서) 여전히 PInvoke로 호출되는 하부 자원들에서 사용되고 있을지도 모르는 TLS는 어떻게 관리가 되는 것인지요?

ICLRTask, ICLRTaskManager 등의 인터페이스가 없는 .NET 1.x에서는 그러한 호스팅을 구현하는 것이 불가능했으며, .NET 2.0에 와서야 그러한 인터페이스가 구현되어 CLR 호스팅을 커스터마이징할 수 있는 기회가 생기긴 했지만, 그조차도 TLS를 사용하는 부분에 대해서 FLS(Fiber local storage)를 사용하도록 바꿔주는 구현이 필요했는데, .NET 1.x에서는 FLS 관련한 코드가 전혀 없기 때문에 "향후의 CLR 버전의 가능성"이 그랬을 뿐, 실제로 1.x에서 가능했던 것은 아닙니다. 그리고, .NET 2.0을 기반으로 한 SQLClr에서 실제로 그러한 구현을 시도했지만, 잘 아시는 것처럼 실제 릴리스 버전에서는 제외되었고요.

설령 FLS를 쓴다고 해도 Fiber 자체가 thread-affinity이기 때문에 서로 다른 Native 스레드에서 접근할 수 있는 배려가 전혀 안 되어 있습니다.




그다음... ^^ 아래의 의견에 대해서 말씀드리면,

"
1.x 시절부터 닷넷 환경에서 운영체제의 thread id는 신뢰할 수 없는 값이였습니다.
"



Fiber 구현 여부를 제외한다면, .NET 1.x/2.0에서 GetCurrentThreadId와 Thread.GetHashCode를 구분할 필요는 없다고 보입니다. 즉, 여전히 신뢰할 수 있는 값입니다. 그렇지 않다면 Obsolete 수준이 아니라 아예 사용할 수 없도록 만들어야 했습니다.

좀 더 자세하게 살펴본다면, 해당 논리 스레드(.NET Managed Thread)가 일단 생성된 이후부터는 철저하게 하나의 동일한 Native 스레드에 매핑되어집니다. 위에서 이미 강조해서 말씀드렸지만, TLS를 사용하는 여러 기반 환경으로 인해 관리 스레드를 여러 개의 Native 스레드에서 스케줄링하는 것은 불가능합니다.

하나의 관리 스레드가 생성되어 그 ID가 3이고, 이때 Native 스레드 ID는 3000이었다고 가정해 보겠습니다. .NET 1.x는 물론이고, 2.0 현재의 버전에서도 TLS로 인해 절대로 관리 스레드(3)를 수행하는 도중, (Suspend 후 Resume을 포함해서) Native 스레드의 ID가 3000 이외의 값이 나오는 경우는 없습니다.

나아가서, 관리 스레드가 종료된 후 GC까지 구동되어 완전하게 관리 스레드 자원이 해제된 경우에는 새로 생성되는 관리 스레드에 대해서 이전의 관리 스레드 ID 3의 경우 및 Native 스레드 ID 3000이 나오는 것이 가능합니다. 결국 스레드가 종료된 이후에는 관리 스레드 ID나 Native Thread ID나 모두 해당 숫자가 재사용된다는 것에는 변함이 없습니다.




현재까지는, 순전히 Fiber의 도입 문제로 인해 ManagedThreadId가 필요한 것을 알 수 있습니다. 하나의 Native 스레드에서 여러 개의 Fiber가 묶여지다 보니, 각각의 Fiber에서 스케줄링 되는 관리 스레드들이 자신의 고유 스레드 ID가 필요하게 되었고, 그런 이유로 인해 .NET에서 ManagedThreadId가 제공되는 것이 의미가 있게 되는 것입니다. 물론... Fiber가 없다면, ManagedThreadId를 쓰나 GetCurrentThreadId를 쓰나 해당 스레드를 구분하는 고유 ID로써 사용하는 것에는 무리가 없고요.

이러저러한 이야기를 종합할 때. 단순히 1.x의 Thread.GetHashCode가 Thread.ManagedThreadId로 온 것에 불과하지만, 실제 뒷 이야기는 Fiber와 관련되어 있다는 것을 "ManagedThreadId ?" 토픽에서 써본 것이었습니다.

그 외, Fiber와 관련된 블로그 토픽을 조금 소개해 봅니다.

Fibers and the CLR 
; https://joeduffyblog.com/2006/11/09/fibers-and-the-clr/
; http://www.bluebytesoftware.com/blog/CommentView,guid,2d0038b5-7ba5-421f-860b-d9282a1211d3.aspx

AppDomain.GetCurrentThreadId() and ManagedThreadId are different 
; http://www.thescripts.com/forum/thread440146.html

How to get thread id for a managed thread?
; https://learn.microsoft.com/en-us/archive/blogs/junfeng/how-to-get-thread-id-for-a-managed-thread

참고로, 관리 스레드가 여전히 TLS 등의 문제로 인해 발생할 수 있는 문제점을 해결하기 위해 다음과 같은 함수들이 ManagedThreadId와 함께 .NET 2.0에 추가되었고요. (물론, 현재는 이런 함수들의 호출 여부가 전혀 영향을 주지 않고 있지만.)

Thread.BeginThreadAffinity Method  
; https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.beginthreadaffinity

Thread.EndThreadAffinity Method  
; https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.endthreadaffinity

제 생각이 틀렸나요???
의견을 부탁드립니다. ^^ 말씀하신 것처럼, 좋은 토론이 되지 않나 싶습니다.



[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/10/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2007-05-24 03시57분
[Loner] 하하... 성태씨한테는 못당하겠군요... 대단하십니다... ^^
먼저 제가 두개 이상의 운영체제 쓰레드가 하나의 Managed Thread와 매핑될 수 있다는
결론을 내리게 된 배경을 설명하지요.

사실, ASP.NET 과 같이 쓰레드 풀을 사용하면서 다수의 AppDomain을 호스팅하는
환경에서 하나의 운영체제 쓰레드가 여러 AppDomain을 서비스 할 수 있을 겁니다.
이 경우, 하나의 운영체제 쓰레드는 두 개 이상의 managed thread 객체에 매핑되지
않나요? 즉, GetCurrentThreadId가 같은 값을 반환하지만 두 Thread 인스턴스는
같지 않게 되죠. 제가 틀렸나요?

이 때문에 1.x 시절부터 GetCurrentThreadId를 100% 신뢰할 수 없다는 생각을
갖었었고, 1.x의 문서에도 GetHashCode를 사용하는 것이 보다 명확하다고 명시되어 있습니다.
(http://msdn.microsoft.com/library/kor/default.asp?url=/library/KOR/cpref/html/frlrfsystemthreadingthreadclasstopic.asp)
다음은 위 문서의 내용중 발췌한 것입니다.

"GetHashCode 는 관리되는 스레드에 대한 ID를 제공합니다. 스레드는 수명 동안, 값을 가져오는 응용 프로그램 도메인에 관계없이 다른 스레드의 값과 충돌하지 않습니다.

참고 관리되지 않는 호스트는 관리되는 스레드와 관리되지 않는 스레드 간의 관계를 제어할 수 있으므로 운영 체제 ThreadId는 관리되는 스레드와 고정 관계가 없습니다. 특히, 정교한 호스트에서는 CLR 호스팅 API를 사용하여 동일한 운영 체제 스레드에 대해 관리되는 여러 스레드를 예약하거나 여러 다른 운영 체제 스레드 사이로 관리되는 스레드를 이동할 수 있습니다."

이 1.x의 MSDN 문서에 의하면 하나의 관리되는 쓰레드가 여러 운영체제 쓰레드와 매핑될 수
있다고 나와 있기 때문에 그런 생각을 갖게 되었죠.
물증도 없이 심증으로만 그런 결론을 내린 저의 접근방법이 옳지 않다는 것은 인정합니다.
하지만 재현이 무쟈게 어렵고 귀찮기 때문에 MSDN을 그대로 믿었다는...

그리고 성태씨 말대로 TLS를 사용하는 Managed thread에 대한 정보를 고려해 보면
두 운영체제 쓰레드가 하나의 Managed thread에 매핑되는 상황은 거의 일어날 수
없을 듯 합니다만... 다시 한번 핑계를 대자면 MSDN 문서에 명시되었던 내용인지라...
그냥 그런가 보다 하고 받아들였던 것으로 기억이 납니다.

GetCurrentThreadId가 Obsolete 수준으로 끝난 이유는 하위 호환성 때문이지,
신뢰할 수 있기 때문이 아니라고 봅니다. 1.x 부터 GetCurrentThreadId는
운영체제 쓰레드의 Id 이지 닷넷 환경의 논리 쓰레드의 Id와는 같지 않기 때문이지요.
이는 1.x 시절부터 이어진 상황인 것만은 확실하며, 정말로 정확하게 따져보자면
운영체제의 ThreadId와 ManagedThreadId는 일대일 매핑이 되지 않는다고 봐야할 듯
싶네요. 그것이 Fiber와 연관이 있건 없건 말이지요.
GetHashCode로서 Managed Thread에 대한 Id로서 사용하는 것이
불합리하다고 판단되어 ManagedThreadId 속성이 추가된 것으로 저는 알고 있으며
Fiber를 위해서만은 아니라고 여전히 생각됩니다.

ManagedThreadId는 GetHashCode 처럼 변화하지 않는 값이 맞습니다.
하나의 Thread 인스턴스는 항상 고유의 ManagedThreadId 값을 반환하겠지요.
닷넷 프로그램은 이 Id 값에 의해 Managed thread를 구분하면 안전할 겁니다.
하지만 동일한 ManagedThreadId 값을 반환했다고 해서 GetCurrentThreadId 가
같은 값을 반환할 것이라고는 100% 장담할 수 없다는 것이 저의 생각입니다.

실제에서는 GetCurrentThreadId와 ManagedThreadId를 구분해야 하는 경우는 99.99%
발생하지 않으므로 저도 실제 코드를 작성할 때는 신경을 쓰지 않습니다.
저는 오히려 GetCurrentThreadId 의 값을 더 선호하는데요, 다양한 unmanaged 디버깅
도구를 사용할 때 편하기 때문이죠. 컴파일러가 경고를 내는 것이 보기 싫어서
P/Invoke 선언을 직접 해서도 사용하기까지 합니다.

의견 주세요. ^^
[guest]
2007-05-25 08시21분
답변이 늦었습니다. ^^

저는 스레드 풀을 "관리 스레드 : Native 스레드"가 1:1로 매핑된 집합으로 여기고 있습니다.

말씀하신 "Thread 관련 웹 문서"에 따르면,

a sophisticated host can use the CLR Hosting API to

(1) schedule many managed threads against the same operating system thread,
(2) move a managed thread between different operating system threads.

위와 같은 내용이 나오는데요. 일단, (1)번의 경우에 대한 제 생각은 순전히 fiber의 도입만이 가능한 것일 뿐, fiber가 아닌 다음에는 동일한 OS Thread를 다수의 관리 스레드에 매핑하는 것은 "현실적으로 불가능"하다고 보입니다. (사실 Fiber 도입의 가장 큰 난제가 TLS의 관리를 FLS로 redirect 시키는 것이 Managed 환경에서는 어느 정도 가능하지만, .NET이 의존하고 있는 수많은 Win32 API 차원에서도 해결되어야 할 문제인지라.)

(2)번의 경우 역시, "현실적으로", "프로그램을 망가뜨릴 각오"를 하고 CLR 호스팅을 커스터마이징하지 않는 이상은 "두 개 이상의 운영체제 스레드가 하나의 Managed Thread와 매핑"되는 경우는 없다고 봅니다. 이 문제 또한, .NET이 의존하고 있는 Win32 차원에서의 해결이 같이 되어야 할 뿐더러, fiber의 경우처럼 FLS가 시스템 차원에서 제공되는 것도 아니기 때문에 (1)번의 경우보다 더욱 어려운 문제일 거라 봅니다.

즉, 저는 여전히 해당 스레드 문서 관련해서 (1), (2)번과 같은 식으로 구현된 CLR 호스팅은 없다고 보고 있기 때문에 "GetCurrentThreadId"와 "ManagedThreadId" 값이 여전히 100% 신뢰할 수 있다는 의견입니다.

만약, 향후에 나올 OS 스레드에 의존하지 않는 CLR이 있다면, 아마도 TLS와 관련된 많은 API들에 대해 사용할 수 없도록 하는 장치가 따라줘야 하지 않을까 예상해봅니다.

말씀하신 스레드 관련해서 OS 스레드에 독립적으로 .NET 관리 스레드가 동작된다는 것은 말 그대로 "이상"일 뿐 가능한 것은 아니며, 실제로 현재 그것이 구현된 예는 전혀 없습니다. 이것은 마치, ".NET Framework"이 OS에 상관없이 올라갈 수 있다는 "이상"이 결국 Windows OS 이외에 어떠한 OS에도 제대로 포팅되지 못했다는 것과 같을 것입니다. 실제로, Fiber 구현에 대해서는 "Thankfully, re-enabling it (for those playing w/ SSCLI) would be a somewhat trivial exercise."와 같은 말에 언급된 것처럼 "SSCLI" 정도일 뿐이고요. 아마도 Microsoft는 SQLCLR을 시작하면서 OS 스레드 독립적인 것을 제거하는 것을 기반으로 다른 OS에 포팅하는 것에 대한 가능성을 점검해 봤을지도 모르겠습니다.
kevin25
2007-05-26 07시09분
[Loner] 네... 사실상 GetCurrentThreadId와 ManagedThreadId 가 1:1 매핑이 안되는 호스트를
"개발"하기는 쉽지 않을 겁니다. 하지만 내가 개발하기 어렵다고 해서
그것을 100% 제외시키는 것 역시 옳지 않다는 생각이지요.
알고서 그것을 쓰는 것과 모르고 쓰는 것은 전혀 다른 얘기가 될 수 있으니까요.
신뢰할 수 없기 때문에 쓰지 말라는 '취지'로 Obsolete로 마킹된 것은 다 이유가
있기 때문아닐까요?
일반적인 콘솔이나 윈폼 어플리케이션과 같은 상황이라면 안전하게
GetCurrentThreadId를 사용할 수 있겠지만 그렇지 않은 상황이 존재할 수 있기
때문이며 이러한 상황은 Fiber를 고려 하지 않더라도 1.1 시절부터 있었다는 것이
제 생각입니다.
그리고... 성태씨 생각같이 "사실상 불가능" 하다라는 결론보다는
그런 상황까지 유발하는 호스트가 거의 없다라는 쪽의 결론이 보다 낫지 않나
싶습니다.
[guest]
2007-05-26 08시20분
넵. 그렇다고 제 글이 ManagedThreadId를 쓰지 말라는 글은 아니었습니다. ^^
첫 번째 글의 마지막 부분을 보시면 저 역시 ^^ Loner 님과 같은 결론을 냈었지요. 단지,,, 글의 취지가 ManagedThreadId를 사용하는 것에 대한 의미가 있을 수 있는 환경에 대한 배경을 설명한 것뿐입니다. ^^

정리해 보면.
어쨌든, 재미있는 차이점이라면, Loner 님은 1.1 시절부터 그런 CLR 호스팅 버전이 있었을 것이라는 의견이시고.
저는 1.x는 물론이고 2.0인 현재까지도 그런 CLR 호스팅 버전은 없을 것이라는 의견이고요.

공통된 결론이라면. 어쨌든 ManagedThreadId를 쓰자는 것이고. ^^
kevin25
2007-09-02 10시40분
오늘 문득. 이 글을 읽고... Katmai에서 구현된 SqlCLR에는 Fiber 모드가 구현되어 있을까... 하는 궁금함이 생겨서 검색을 해봤습니다.

Integrated CLR and fiber-mode
; (broken) http://forums.microsoft.com/TechNet/ShowPost.aspx?PostID=1914052&SiteID=17
kevin25

1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13576정성태3/8/20241543닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20241676닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20241557닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/20241563닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/20241639닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/20241600닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/20241627닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/20241539닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/20241602닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/20241613오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/20241626오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20241464닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
13564정성태2/22/20241614Windows: 259. Hyper-V Generation 1 유형의 VM을 Generation 2 유형으로 바꾸는 방법
13563정성태2/21/20241644디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
13562정성태2/21/20241643오류 유형: 896. ASP.NET - .NET Framework 기본 예제에서 System.Web에 대한 System.IO.FileNotFoundException 예외 발생
13561정성태2/20/20241741닷넷: 2218. C# - (예를 들어, Socket) 비동기 I/O에 대한 await 호출 시 CancellationToken을 이용한 취소파일 다운로드1
13560정성태2/19/20241744디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
13559정성태2/19/20242623오류 유형: 895. ASP.NET - System.Security.SecurityException: 'Requested registry access is not allowed.'
13558정성태2/18/20241819닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
13557정성태2/18/20241572Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20241637Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20241951닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20241707VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20241734닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20241683닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20242016닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...