现在的位置: 首页 > 综合 > 正文

《你不常用的c#之五》:Thread与ThreadPool的内存之战

2012年11月24日 ⁄ 综合 ⁄ 共 15190字 ⁄ 字号 评论关闭

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

那现在又出现问题拉,既然ThreadPool这么好,那我们为啥还使用Thread呢?这个问题就是ThreadPool有个GetMaxThreads,可以通过GetMaxThreads(out int workerThreads, out int completionPortThreads);方法获取到,如果线程池满拉,则会死锁更严重!
另:ThreadPool都为后台线程。
究竟使用那个,根据情况而定,理解拉内在的东西,一切表象就简单拉。
OK,到此吧。。。

希望本文能对你有所帮助,谢谢!

Taheta 标签: ,,
1
0
(请您对文章做出评价)
« 上一篇:www.crashIE.com–IE崩溃啦
» 下一篇:如何使用vs自带的Spy++来查看当前线程切换次数(Context Switch)
posted @ 2009-01-06 22:41 overred Views(4470) Comments(31) Edit 收藏

 回复 引用 查看   

2009-01-06 22:52 | killkill      

哦,想不到啊,终于找到一个深入分析的文章。
 回复 引用 查看   

>楼2009-01-06 23:52 | 5yplan      

很好,就需要这样的文章。
再细细看看。
 回复 引用   

&楼2009-01-07 00:06 | -tian-[未注册用户]

谢谢楼主的文章,正需要这样的文章呢。
 回复 引用 查看   

*楼2009-01-07 00:32 | Jeffrey Zhao      

哈,不错,很好的侧面啊。
我喜欢这样的文章,相较于那些似乎很深入,但是只是在为了罗列而罗列,却没有解决什么问题说明什么事情的文章。
 回复 引用 查看   

o楼2009-01-07 07:38 | eaglet      

我试了一下,第一次GC时确实无法将这些死线程清除掉。
不过楼主的代码有点小问题
在调用 GC.Collect(); 前需要等待一段时间才能成功。

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();

}

Thread.Sleep(5000);//等待5秒
GC.Collect();

Thread.Sleep(5000);//等待5秒
GC.Collect();

Console.Read();

}

 回复 引用 查看   

O楼2009-01-07 08:23 | S.Sams      

--引用--------------------------------------------------
-tian-: 谢谢楼主的文章,正需要这样的文章呢。
--------------------------------------------------------
me to!
 回复 引用 查看   

s楼2009-01-07 08:39 | 非空      

死线程还能唤醒吗?
 回复 引用 查看   

S楼2009-01-07 08:52 | airwolf2026      

哇.非常感谢楼主这样的文章哈.
俺也很想了解这些底层的东西.但是限于时间等...(其实是懒,(*^__^*) 嘻嘻……)

现在总算明白为啥用线程池里面的比自己直接New Thread好了.嘎嘎

 回复 引用 查看   

e楼2009-01-07 08:55 |       

学习了.虽然有些地方看不太懂.
 回复 引用 查看   

2009-01-07 08:57 | airwolf2026      

对啦,曾经在测试一个异步TCP程序的时候(一个和终端通讯的程序,就一条协议),整个异步方法里面加了个Lock,在异步方法里面出bug,弹出了一个Messagebox.结果第二天来看,发现这个程序耗了1G多内存....再看原因,既然它占用1k多的线程....
 回复 引用 查看   

2009-01-07 09:09 | Kevin-moon      

够深入!
最近正在研究内存这个东东 文章很不错
 回复 引用 查看   

2009-01-07 09:37 | winzheng      

深刻,顶。
 回复 引用 查看   

[楼主]2009-01-07 09:51 | overred      

谢谢各位
@eaglet
Sleep后GC清理FinalizeQueue线程开始启动,所以deadthread会根据你sleep的长短而数量不同
谢谢

@非空
这个例子里的线程不会唤醒拉。
因为内核对象中的context挂起计数已经大于0,当windows每隔大概 20ms(线程优先级度相同)扫一次,发现这些都不需要调度,不会分配任何cpu时间给他们。

 回复 引用 查看   

[楼主]2009-01-07 09:57 | overred      

@airwolf2026
异步TCP的时候可以直接使用IOCP,这玩意据说是ms windows开发组搞拉很长时间想出的一个东东。
给你推荐几篇文章参考:
IOCP Thread Pooling in C#
地址:
http://www.devarticles.com/c/a/C-Sharp/IOCP-Thread-Pooling-in-C-sharp-Part-I/

http://www.devarticles.com/c/a/C-Sharp/IOCP-Thread-Pooling-in-C-sharp-Part-II/

 回复 引用 查看   

2009-01-07 10:09 | TerryLee      

不错的文章,支持overred:)

PS:现在回北京了吗?

 回复 引用 查看   

[楼主]2009-01-07 10:11 | overred      

@TerryLee
嘻嘻。。。回拉
谢谢支持
 回复 引用 查看   

2009-01-07 12:36 | Angel Lucifer      

我来唱下反调。这篇文章的出发点有点莫名其妙。

ThreadPool 本身正是为了减少线程使用数量,避免过多上下文切换才设计出来的。内存占用肯定会比新建大幅数量的线程少的多。
这在 C++ 世界几乎是常识。凡是显式内存管理的编程语言,池化技巧是必备技能。我们在 .NET Framework 也可以多处看到此类技巧的应用。

所以我说这两个拿来比较有点莫名其妙。

PS : 如果想要更清楚的看到 GC 回收资源。可以在 GC.Collect() 后紧跟着调用 GC.WaitForPendingFinalizers() 方法。

 回复 引用 查看   

[楼主]2009-01-07 12:41 | overred      

@Angel Lucifer
首先谢谢你的观点
你说的对

我就是对内存中的他俩对象的分布出发。。。
当我们在使用这俩东东的时候,内存中发生拉什么变化
比如我在使用Thread的时候为啥有上千个不会GC掉,而且占内存
对于这个相信windows核心编程讲的更明白。。。。

诸如 GC.Collect() 和GC.WaitForPendingFinalizers() 等方法在我们的编程中不应该作为一个方法使用(除非迫不得已),不然clr的GC机制不是浪费拉

呵呵
这不是反调,是在交流观点,文中有不对之处,还望斧正那。。。
thx

 回复 引用 查看   

[楼主]2009-01-07 12:56 | overred      

@Angel Lucifer
补充一点
接调用GC.WaitForPendingFinalizers() 其实它应该就是在等clr的第二次GC
因为所有Thread的析构执行完毕他才会从FinalizeQueue里彻底移除

而WaitForPendingFinalizers正是挂起当前线程,等待第二次GC的时候,直到专门清理FinalizeQueue的线程对Queue彻底清理完毕,然后再返回。。。。这样世界就清净拉
Suspends the current thread until the thread processing the queue of finalizers has emptied that queue.

 回复 引用 查看   

>0楼2009-01-07 13:31 | Angel Lucifer      

@overred
不过也应当注意一点,就是 .NET Framework 提供的线程池效率中庸。如果想要进一步提升程序性能,最好还是写一个专用线程池。 至于 GC ,《CLR via C#》讲的很清楚,很详细。
 回复 引用 查看   

>1楼[楼主]2009-01-07 14:53 | overred      

@Angel Lucifer
校友好
.NET Framework 提供的线程池效率中庸的一大原因就是提供第三方保障,如内存和线程安全

其实IOCP不光用在IO上,用在Ap上一样很好。
,《CLR via C#》的作者老Jeffrey就很是IOCP的忠实拥护者
这点在他的核心编程里他都情不自禁的提起。。

抱歉!评论已关闭.