Thread, System.Threading.Thread, and !Threads (I)

If you use SOS’s !Threads command during debugging a lot,  you should be familiar with such output:

0:003> !threads
PDB symbol for mscorwks.dll not loaded
Loaded Son of Strike data table version 5 from "C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll"
ThreadCount: 12
UnstartedThread: 5
BackgroundThread: 1
PendingThread: 0
DeadThread: 5
PreEmptive GC Alloc Lock
ID ThreadOBJ State GC Context Domain Count APT Exception
0 0xb74 0x0014f230 0x20 Enabled 0x00000000:0x00000000 x00149aa8 1 Ukn
2 0xb58 0x00157cf8 0x1220 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn (Finalizer)
XXX 0 0x001665f0 0x1820 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016d348 0x1400 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016d510 0x1820 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016d9d0 0x1400 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016db98 0x1820 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016e248 0x1400 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016e410 0x1820 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016e740 0x1400 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016e908 0x1820 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn
XXX 0 0x0016ec98 0x1400 Enabled 0x00000000:0x00000000 x00149aa8 0 Ukn

 
Have you ever wondered what exactly are in the list? Why the number of threads listed here doesn’t match the number of "real" threads in the process (in the example above, I only have 4 threads in the process, but !Threads shows 12)? Why some "real" threads have entries here, some don't? Maybe they are managed System.Threading.Thread objects (but the number in the list might not match number of Thread objects either)? Answers to those questions are tied to how CLR implements threads and manages thread information.
 
CLR provides classes in System.Threading namespace as threading APIs. As you know, threading is implemented by utilizing native threads in underlying OS (Windows, in CLR case). CLR just piggybacks managed threads to native OS threads. Users use BCL System.Threading.Thread objects (I’ll call it as C# Thread below) to control managed threads just as thread HANDLE is used to control to windows native threads. If you ever checked contents of a C# Thread object, you will find it is quite small (here I use SOS !DumpObj command, you could also see it using Visual Studio or other managed debuggers):
 

0:003> !DumpObj 0x00c03248
Name: System.Threading.Thread
MethodTable 0x79bb8384
EEClass 0x79bb85b0
Size 60(0x3c) bytes
GC Generation: 0
mdToken: 0x020000eb (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
FieldDesc*: 0x79bb8614
MT Field Offset Type Attr Value Name
0x79bb8384 0x4000330 0x4 CLASS instance 0x00000000 m_Context
0x79bb8384 0x4000331 0x8 CLASS instance 0x00000000 m_LogicalCallContext
0x79bb8384 0x4000332 0xc CLASS instance 0x00000000 m_IllogicalCallContext
0x79bb8384 0x4000333 0x10 CLASS instance 0x00000000 m_Name
0x79bb8384 0x4000334 0x14 CLASS instance 0x00000000 m_ExceptionStateInfo
0x79bb8384 0x4000335 0x18 CLASS instance 0x00000000 m_Delegate
0x79bb8384 0x4000336 0x1c CLASS instance 0x00000000 m_PrincipalSlot
0x79bb8384 0x4000337 0x20 CLASS instance 0x00000000 m_ThreadStatics
0x79bb8384 0x4000338 0x24 CLASS instance 0x00000000 m_ThreadStaticsBits
0x79bb8384 0x4000339 0x28 CLASS instance 0x00000000 m_CurrentCulture
0x79bb8384 0x400033a 0x2c CLASS instance 0x00000000 m_CurrentUICulture
0x79bb8384 0x400033b 0x30 System.Int32 instance 2 m_Priority
0x79bb8384 0x400033c 0x34 System.Int32 instance 1498008 DONT_USE_InternalThread
0x79bb8384 0x400033d 0 CLASS shared static m_LocalDataStoreMgr

 

CLR actually needs much more information about a thread than fields of the C# Thread class, and such information is needed in CLR's unmanaged part where it's not easy to access managed objects. So inside Execution Engine (so far Execution Engine is still written in unmanaged code), there is an unmanaged C++ class also called Thread (let me call  it C++ Thread) to keep all information for an OS native thread (OS thread). In Rotor, this class is defined in vm\threads.h.
 
CLR needs to create a C++ Thread object for every OS thread EE knows of, that is, every thread which ever ran managed code. Such threads could either be (a) created explicitly by users using C# Thread.Start, (b) an unmanaged OS thread who has ever visited managed world, e.g, through interop, or (c) a special OS thread in CLR which might run managed code. For case a, when a user creates a C# Thread object, CLR will create a C++ Thread object, link it to the C# Thread object, and mark it as unstarted (Rotor: SetupUnstartedThread in vm\threads.cpp). Once Start method is called on the C# Thread object, CLR will create an OS thread, in the OS thread’s ThreadPproc (Rotor: ThreadNative::KickOffThread in vm\ComSynchronizable.cpp), CLR will save the C++ Thread object’s address to the OS thread's TLS (Thread Local Storage) and mark it to be started (Rotor: ThreadStore::TransferStartedThread in vm\threads.cpp); For case b, at entry point for an unmanaged OS thread to managed world, CLR will create a C++ Thread object and also use TLS to associate it with the OS thread (Rotor: SetupThread in vm\threads.cpp); Case c is similar to case b, except CLR might set up C++ Thread earlier.
 
In any case, the C++ Thread object is the primary place for CLR to store information regarding to a managed thread. When CLR needs to access the information, it will just fetch the object from the OS thread's TLS. In that sense, the C# Thread is more like a managed proxy for the C++ Thread. In fact, for case b and c, there will be no C# Thread objects created unless users call System.Threading.Thread.CurrentThread. C++ Thread tracks its corresponding C# Thread using a GCHandle (m_ExposedObject field in C++ Thread object); C# Thread tracks its C++ Thread using a native pointer(DONT_USE_InternalThread field in C# Thread). You could verify this circular reference in debugger if you have symbols for mscorwks.dll:
 

0:003> !do 0x00c03248
Name: System.Threading.Thread

MT Field Offset Type Attr Value

0x79bb8384 0x400033c 0x34 System.Int32 instance 1498008 DONT_USE_InternalThread


0:013> dt mscorwks!Thread 0n1498008

+0x0c0 m_ExposedObject : 0x00a710e4


0:013> dp 0x00a710e4 l1
00a710e4 00c03248

 
CLR uses a data structure called Thread Store (Rotor: ThreadStore in vm\threads.h) to keep all C++ Threads. What you see in output of !Threads command is actually list of C++ Threads in Thread Store. The “ThreadOBJ” field is address of a C++ Thread object. Other fields are important information about the C++ Thread, including OS thread ID for the corresponding OS thread. Here you could see not every live OS thread has a C++ Thread, which could be explained by the fact that CLR only creates a C++ Thread for an OS thread which ever runs managed code. Meanwhile you may also find some C++ Threads don't have an OS thread associated with them (the ID fields are “XXX”). They are either (a)unstarted, (b) failed to started, (c)used to represent an live OS thread which is now dead. For case (a), CLR will wait until C# Thread.Start is called and create an OS thread for it then; for (b) and (c), the C++ Threads object will be deleted some time later.

 

This posting is provided "AS IS" with no warranties, and confers no rights.