《你不常用的c#之五》:Thread与ThreadPool的内存之战
Thread与ThreadPool使用的时候在内存里对象是如何分布的呢?
今天我们就从内存堆的角度分析下两者。
先上小白鼠代码:
static void Main(string[] args) { for (int i = 0; i < 30; i++) { Thread t = new Thread(new ThreadStart(ThreadProc)); t.Name = "Overred_" + i; t.Start(); } Console.Read(); } static void ThreadProc() { try { for (int i = 0; i < 10; i++) { Console.WriteLine("{0} Value:{1}",Thread.CurrentThread.Name,i); } } catch (Exception ex) { Console.WriteLine(ex.Message); } }以上代码非常简单,就是循环启动30个线程去执行同一个方法ThreadProc(),然后打印出结果。 现在提出问题1:当Main里的30个线程都把ThreadProc()方法执行完毕后,这些Threads是自动消亡还是被GC回收,还是变成DeadThread? 好,拿出我们的看家工具windbg,来debug一把。 首先启动我们的程序,然后打开windbg,然后F6,Attach我们的exe 1,加载mscorwks(.net 2.0或者以上)
0:003> .loadby sos mscorwks
2,查看该程序的线程情况
*** ERROR: Symbol file could not be found. Defaulted to export symbols for
C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll –
PDB symbol for mscorwks.dll not loaded
ThreadCount: 32
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 30
Hosted Runtime: no
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 25e4 00518858 a020 Enabled 013f878c:013f9fe8 00514818 1 MTA
2 2 24b8 00526f20 b220 Enabled 00000000:00000000 00514818 0 MTA (Finalizer)
XXXX 3 0 00533028 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 4 0 00536858 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 5 0 005385c8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 6 0 005393d0 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 7 0 00534fd8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 8 0 0053a5c0 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 9 0 0053b3c8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX a 0 0053bfc0 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX b 0 0053eba8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX c 0 00543370 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX d 0 00543b38 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX e 0 00544700 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX f 0 00544ec8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 10 0 00545690 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 11 0 00545ee0 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 12 0 005466c0 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 13 0 00546a88 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 14 0 00546e50 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 15 0 00547218 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 16 0 005475e0 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 17 0 005479a8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 18 0 00547d70 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 19 0 00548138 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 1a 0 00548500 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 1b 0 005488c8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 1c 0 00548c90 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 1d 0 00549058 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 1e 0 00549420 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 1f 0 005497e8 9820 Enabled 00000000:00000000 00514818 0 Ukn
XXXX 20 0 00549bb0 9820 Enabled 00000000:00000000 00514818 0 Ukn
看红色加粗部分,我们总共有32个线程,而DeadThread为30个(其他2个为程序自身所有,其中一个BackgroundThread), 先告诉你这30个死线程正式我们循环创建的线程,可以回答我提的第一个问题拉,没错,他们统统死拉,而且不会醒来,还占地方(不是永远占地方,待会我们用 GC手动让它们消亡)。
3,然后我们继续看看内存堆上它们这些坏家伙如何分布:
total 155 objects
Statistics:
MT Count TotalSize Class Name
79108930 1 32 System.Threading.ContextCallback
790fe284 2 144 System.Threading.ThreadAbortException
79124b74 30 600 System.Threading.ThreadHelper
79104de8 31 1116 System.Threading.ExecutionContext
790fe704 31 1736 System.Threading.Thread
791249e8 60 1920 System.Threading.ThreadStart
Total 155 objects
4,我们来具体看下这些Thread们的MethodTable
Address MT Size
013c1708 790fe704 56
013c178c 790fe704 56
013c235c 790fe704 56
013c2474 790fe704 56
013c258c 790fe704 56
013c26a4 790fe704 56
013c27bc 790fe704 56
013c28d4 790fe704 56
013c29ec 790fe704 56
013c2b04 790fe704 56
013c2c1c 790fe704 56
013c2d34 790fe704 56
013c2e54 790fe704 56
013c2f74 790fe704 56
013c3094 790fe704 56
013c31b4 790fe704 56
013c32d4 790fe704 56
013c33f4 790fe704 56
013c3514 790fe704 56
013c3634 790fe704 56
013c3754 790fe704 56
013c3874 790fe704 56
013c3994 790fe704 56
013c3ab4 790fe704 56
013c3bd4 790fe704 56
013c3cf4 790fe704 56
013c3e14 790fe704 56
013c3f34 790fe704 56
013f8084 790fe704 56
013f81a4 790fe704 56
013f82c4 790fe704 56
total 31 objects
Statistics:
MT Count TotalSize Class Name
790fe704 31 1736 System.Threading.Thread
Total 31 objects
5,随便拿一个线程的Address来看看到底是谁占着我们的Thread而不让我们的GC回收掉
0:003> !GCRoot 013c3bd4 Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info. Scan Thread 0 OSTHread 25e4 Scan Thread 2 OSTHread 24b8 DOMAIN(00514818):HANDLE(WeakSh):241298:Root:013c3bd4(System.Threading.Thread)
结果另我们很失望,他自己就是根,并没被其他任何对象所引用,什么情况下会出现此情况呢?我们先来看看对象在内存中分布的几种方式,我们只需在windbg里执行如下命令则知:
——————————————————————————-
!GCRoot [-nostacks] <Object address>
!GCRoot looks for references (or roots) to an object. These can exist in four
places:
1. On the stack
2. Within a GC Handle
3. In an object ready for finalization
4. As a member of an object found in 1, 2 or 3 above.
First, all stacks will be searched for roots, then handle tables, and finally
the freachable queue of the finalizer. Some caution about the stack roots:
!GCRoot doesn't attempt to determine if a stack root it encountered is valid
or is old (discarded) data. You would have to use !CLRStack and !U to
disassemble the frame that the local or argument value belongs to in order to
determine if it is still in use.
Because people often want to restrict the search to gc handles and freachable
objects, there is a -nostacks option.
一个对象可以
1,在栈上
2,在一个GCHandle里(可以执行!GCHandles命令查看)
3,在FinalizeQueue里
4,是一个对象的成员
难道对象就必定在以上的“四行”之中吗?答案是不一定,还有个Gchandleleaks,就是你在内存里看不到这个Handle,它已经leak。(这种也算在GCHandle里吧)。
回头我们接着说他自己没被其他任何对象所引用,自己就是个根,但是GC却不搭理它,为何?那就是他在GCHandle里,
GC Handle Statistics:
Strong Handles: 14
Pinned Handles: 4
Async Pinned Handles: 0
Ref Count Handles: 0
Weak Long Handles: 0
Weak Short Handles: 31
Other Handles: 0
Statistics:
MT Count TotalSize Class Name
790fd0f0 1 12 System.Object
790fcc48 1 24 System.Reflection.Assembly
790feba4 1 28 System.SharedStatics
790fe17c 1 72 System.ExecutionEngineException
790fe0e0 1 72 System.StackOverflowException
790fe044 1 72 System.OutOfMemoryException
790fed00 1 100 System.AppDomain
79100a18 4 144 System.Security.PermissionSet
790fe284 2 144 System.Threading.ThreadAbortException
790fe704 32 1792 System.Threading.Thread
7912d8f8 4 8736 System.Object[]
Total 49 objects
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
———————————-
generation 0 has 35 finalizable objects (00526658->005266e4)
generation 1 has 0 finalizable objects (00526658->00526658)
generation 2 has 0 finalizable objects (00526658->00526658)
Ready for finalization 0 objects (005266e4->005266e4)
Statistics:
MT Count TotalSize Class Name
791037c0 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
79103764 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
79101444 2 40 Microsoft.Win32.SafeHandles.SafeFileHandle
790fe704 31 1736 System.Threading.Thread
Total 35 objects
啥身份,就是自身实现拉析构函数。
具体原理大家可以看.net框架去,我这里不多说。
说到此,也就找到我们当初30个彪形大汉为啥赖着不走的原因拉,是在0代的第一次GC时候,他们被放进FinalizeQueue,等着第二次GC他们部分才会从内存堆上消亡。
为证明我们的观点,我们可以修改程序为 :
static void Main(string[] args) { for (int i = 0; i < 30; i++) { Thread t = new Thread(new ThreadStart(ThreadProc)); t.Name = "Overred_" + i; t.Start(); } GC.Collect(); GC.Collect(); Console.Read(); }
首先声明一点就是当我们调用一次GC.Collect();时,并不是执行一次垃圾收集,只是告诉系统我要强制进行垃圾收集,系统听到这个命令后乖不乖那就不一定拉。
当我们用Reflector查看mscorlib对Thread实现的使用也会发现他实现拉析构:
~Thread() { this.InternalFinalize(); }
来个虎头蛇尾吧,当我们把小白鼠程序使用ThreadPool修改为:
{
for (int i = 0; i < 30; i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));
}
Console.Read();
}
static void ThreadProc(object o)
{
try
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(" Value:{0}",i);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
*** ERROR: Symbol file could not be found. Defaulted to export symbols
for C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll –
PDB symbol for mscorwks.dll not loaded
ThreadCount: 4
UnstartedThread: 0
BackgroundThread: 3
PendingThread: 0
DeadThread: 0
而FinalizeQueue则为:
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
———————————-
generation 0 has 7 finalizable objects (00266658->00266674)
generation 1 has 0 finalizable objects (00266658->00266658)
generation 2 has 0 finalizable objects (00266658->00266658)
Ready for finalization 0 objects (00266674->00266674)
Statistics:
MT Count TotalSize Class Name
791037c0 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
79103764 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
79101444 2 40 Microsoft.Win32.SafeHandles.SafeFileHandle
790fe704 3 168 System.Threading.Thread
Total 7 objects
为啥还有3个,呵呵,这你可以继续用反射工具查看ThreadPool的实现,在其核心代码ExecutionContext : ISerializable里使用拉一个Thread。
那现在又出现问题拉,既然ThreadPool这么好,那我们为啥还使用Thread呢?这个问题就是ThreadPool有个 GetMaxThreads,可以通过GetMaxThreads(out int workerThreads, out int completionPortThreads);方法获取到,如果线程池满拉,则会死锁更严重!
另:ThreadPool都为后台线程。
究竟使用那个,根据情况而定,理解拉内在的东西,一切表象就简单拉。
OK,到此吧。。。
希望本文能对你有所帮助,谢谢!