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,查看该程序的线程情况
0:003> !Threads
*** 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,然后我们继续看看内存堆上它们这些坏家伙如何分布:
0:003> !DumpHeap -type System.Threading -stat
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
红色部分,31个Thread,对应着31个Context,每个线程在windows底层都是一个内核对象和一个栈空间,内核对象存放一些线程的统计信息,比如计数器以及一个上下文,就是我上次执行到那里等。而栈空间则是用来存放线程参数等。
4,我们来具体看下这些Thread们的MethodTable
0:003> !DumpHeap -MT 790fe704
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里执行如下命令则知:
0:003> !Help gcroot
-------------------------------------------------------------------------------
!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.
windbg已经很清楚的告诉我们,
一个对象可以
1,在栈上
2,在一个GCHandle里(可以执行!GCHandles命令查看)
3,
在FinalizeQueue里
4,是一个对象的成员
难道对象就必定在以上的“四行”之中吗?答案是不一定,还有个Gchandleleaks,就是你在内存里看不到这个Handle,它已经leak。(这种也算在GCHandle里吧)。
回头我们接着说他自己没被其他任何对象所引用,自己就是个根,但是GC却不搭理它,为何?那就是他在GCHandle里,
0:003> !GCHandles
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
而且在FinalizeQueue里也有它的踪影:
0:003> !FinalizeQueue
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
下面就来解释下什么才可以在FinalizeQueue里出现呢?答案就是有身份的人,很有身份的人,享受特殊待遇的哦!
啥身份,就是自身实现拉析构函数。
啥待遇,就是GC两次才有可能把他们部分清理掉!为啥部分,是我们不知道windows到底何时去把所有的清理掉(赖皮阿)
具体原理大家可以看.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修改为:
static
void
Main(
string
[] args)
{
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);
}
}
再用windbg查看线程时则为:
0:006> !Threads
*** 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则为:
0:006> !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,到此吧。。。
希望本文能对你有所帮助,谢谢!
http://www.taheta.org/
Code
http://www.taheta.org