DiggingDeeply

try...catch...finally中的finally一定会执行吗?

在windows中,每个线程默认的栈大小是1M,托管线程也一样。

在32位windows中,用C#在系统中最多可以创建多少个线程呢?答案稍后说。

大家都知道try...catch...finally是用来控制异常的流转,一般说来finally是最后一班岗哨,问100个人,99个肯定说一定会执行。

是的,一般来讲确实是能执行到的,原因是什么呢?比如在try或catch里return之后为什么还能执行到finally呢?答案是因为return只是把返回值放入相应的地方(一般来讲是寄存器),准备返回;在一个函数返回之前,也就是ret指令调用之前,还有一些代码需要执行,就是清空堆栈(弹出入栈的压入的参数,函数的首地址等),这和函数的调用方式stdcal有关。

那么finally有没有机会不执行呢?看代码:

 

 

 

 

 1     static  void Foo()
 2     {
 3         Foo();
 4     }
 5 
 6     static void Main()
 7     {
 8         int i=0;
 9         try
10         {
11             Foo();   
12         }
13         catch(Exception ex)
14         {
15             ;
16         }
17         finally
18         {
19             i = 2;
20         }
21         
22     }

 

 

这个递归调用会造成System.StackOverflowException,但是我下面有catch啊?为什么就直接程序直接就结束了呢?

下面用windbg调试一下:

 

CommandLine: "C:\Documents and Settings\Administrator\My Documents\Visual Studio 2008\Projects\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe"
Symbol search path 
is: c:\symbols;http://msdl.microsoft.com/download/symbols;C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\symbols
Executable search path is: c:\symbols
ModLoad: 
00400000 00408000   ConsoleApplication2.exe
ModLoad: 7c930000 7ca02000   ntdll.dll
ModLoad: 
79000000 79046000   C:\WINDOWS\system32\mscoree.dll
ModLoad: 7c800000 7c92b000   C:\WINDOWS\system32\KERNEL32.dll
(bf8.4ec): Break instruction exception 
- code 80000003 (first chance)
eax
=7ca00000 ebx=7ffdf000 ecx=00000001 edx=00000002 esi=7c9b97f4 edi=00151f38
eip
=7c94a3e1 esp=0012fb70 ebp=0012fcb4 iopl=0         nv up ei pl nz na po nc
cs
=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll
!DbgBreakPoint:
7c94a3e1 cc              
int     3
0:000> g
ModLoad: 77f30000 77fdc000   C:\WINDOWS\system32\ADVAPI32.dll
ModLoad: 77c20000 77cbf000   C:\WINDOWS\system32\RPCRT4.dll
ModLoad: 76eb0000 76ec3000   C:\WINDOWS\system32\Secur32.dll
ModLoad: 77eb0000 77f02000   C:\WINDOWS\system32\SHLWAPI.dll
ModLoad: 77bd0000 77c19000   C:\WINDOWS\system32\GDI32.dll
ModLoad: 77e10000 77ea0000   C:\WINDOWS\system32\USER32.dll
ModLoad: 77b70000 77bca000   C:\WINDOWS\system32\msvcrt.dll
ModLoad: 
76180000 7619d000   C:\WINDOWS\system32\IMM32.DLL
ModLoad: 7f000000 7f009000   C:\WINDOWS\system32\LPK.DLL
ModLoad: 74ae0000 74b45000   C:\WINDOWS\system32\USP10.dll
ModLoad: 
48000000 48020000   C:\PROGRA~1\Google\GOOGLE~2\GOEC62~1.DLL
ModLoad: 71b60000 71b77000   C:\WINDOWS\system32\WS2_32.dll
ModLoad: 71b50000 71b58000   C:\WINDOWS\system32\WS2HELP.dll
ModLoad: 79e70000 7a3ff000   C:\WINDOWS\Microsoft.NET\Framework\v2.
0.50727\mscorwks.dll
ModLoad: 
78130000 781cb000   C:\WINDOWS\WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.1433_x-ww_5CF844D2\MSVCR80.dll
ModLoad: 7ca10000 7d1ec000   C:\WINDOWS\system32\shell32.dll
ModLoad: 77cd0000 77dd3000   C:\WINDOWS\WinSxS\x86_Microsoft.Windows.Common
-Controls_6595b64144ccf1df_6.0.3790.3959_x-ww_D8713E55\comctl32.dll
ModLoad: 790c0000 79bf6000   C:\WINDOWS\assembly\NativeImages_v2.
0.50727_32\mscorlib\32e6f703c114f3a971cbe706586e3655\mscorlib.ni.dll
ModLoad: 774b0000 775e9000   C:\WINDOWS\system32\ole32.dll
ModLoad: 
79060000 790b6000   C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorjit.dll
(bf8.4ec): Stack overflow 
- code c00000fd (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax
=00a83028 ebx=0012f4ac ecx=00000064 edx=00000000 esi=00000064 edi=00000000
eip
=00f90110 esp=00033000 ebp=0012f480 iopl=0         nv up ei pl zr na pe nc
cs
=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
00f90110 
56              push    esi

 

大家可以看到起始的时候esp是在0012fb70,ebp是在0012fcb4;而异常发生的时候esp在00033000,而ebp在0012f480可以发现两个问题:

1.0012fb70-00033000=fcb70(十进制是1035120>1MB),所以溢出了

2.栈是向上增长的,逆着内存地址增长的顺序,所以esp会不停的减小,而ebp在esp下面固定每一次方法调用的基地址。但是这个时候ebp已经是0012f480了,超过了进入Foo之前的esp的地址,溢出了,所以回到了栈底。

这个时候callstack已经完全回不去了,也就是说方法链不能依着原来的路径返回到调用方法之外,就出现了无法恢复的异常,CLR直接把主线程给kill了,结果进程就退出了。

换段代码:

 

 1     static  void Foo()
 2     {
 3         Foo();
 4     }
 5 
 6     static void Main()
 7     {
 8         int i=0;
 9         try
10         {
11             Thread t1 = new Thread(new ThreadStart (Foo));
12             t1.Start();
13         }
14         catch(Exception ex)
15         {
16             ;
17         }
18         finally
19         {
20             i = 2;
21         }
22         
23     }

 

这个代码里的finally是可以运行的,因为溢出的不是主线程的堆栈,所以kill了也没事。但是同样会抛出System.StackOverflowException异常。

 当然,可以通过参数来控制线程最大堆栈大小。可以通过一些tool来观察这些线程的活动,由于时间有限,我就不做了,赶紧洗洗上班去。

最后公布答案:用C#创建用户级的线程(如果你非得较真用P/V Invoke来创建内核级线程,我无话可说,也不要用boot.ini /3GB)极限是2048个(往往到不了这个数),因为32系统只有2G内存用于用户态程序,而每个线程默认的栈大小是1M,所以2G/1M=2048个,大家可以运行下面的代码测试一下:

 

 1 static  void Foo()
 2     {
 3         Thread.Sleep(Int32.MaxValue);
 4     }
 5 
 6     static void Main()
 7     {
 8         
 9         try
10         {
11             for (int i = 0; i < Int32.MaxValue; i++)
12             {
13                 Thread t1 = new Thread(new ThreadStart(Foo));
14                 Console.WriteLine(i.ToString ());
15                 t1.Start();
16             }
17         }
18         catch(Exception ex)
19         {
20             Console.WriteLine(ex.ToString ());
21         }
22         finally
23         {
24            
25         }
26         
27     }

这段代码来自jeff的一个presentation。在我的机器上运行的是:

 

不对之处,欢迎大家指正。

0
0
(请您对文章做出评价)
« 上一篇:汽车人,变形,出发!
» 下一篇:try...catch...finally中的finally一定会执行吗?(二,完结篇)

posted on 2009-07-03 07:30 DiggingDeeply 阅读(3260) 评论(36)  编辑 收藏 网摘

评论

#1楼 2009-07-03 07:37 韦恩卑鄙      

在32位windows中,用C#在系统中最多可以创建多少个线程呢?
这应该是一个纯粹学术性问题

上次 jeffery richard 在.net大会上说  如果一个开发人员在开发过程中,问了自己这样一个问题,那么这个开发人员已经 go very wrong了。

out look 2007 平时需要50多个线程 已经算是巨无霸了。
  回复  引用  查看    

#2楼 2009-07-03 07:53 Silverlight SDK2 Template[未注册用户]

楼主对Fianlly那块的解释还真是新鲜,原来根本都没想到过这种异常,长见识了:)   回复  引用    

#3楼 2009-07-03 08:21 @韦恩卑鄙[未注册用户]

@韦恩卑鄙
如果是服务器端程序,那可不是50个线程能解决问题的。

楼主你说说如何修改线程的堆栈的默认大小。我需要改的小一下,节省一些内存?
  回复  引用    

#4楼 2009-07-03 08:23 补丁      

学术讨论可以,实际意义没想出来
我估计你要是问我同样问题,我就说,不是
我会反问一个问题
在catch段里抛出一个异常,finally还运行么?
  回复  引用  查看    

#5楼 2009-07-03 08:51 AutumnWinter      

记得看过一篇老外的文章,面试的人问他什么时候finally不会被执行,他不加思索的说:Power off 的时候,结果,被录取。   回复  引用  查看    

#6楼 2009-07-03 09:00 devil0153      

很有用,最近遇到CLR直接崩溃的情况,google了半天发现StackOverflow是捕获不到的,还有GC异常也是捕获不到的,还有一些异常是在线程中抛出的,但如果没有代码处理,主程序根本不与理会,以前还真不知道这些知识!主要是太不常见了,但真的遇到这样的问题真的很难排错   回复  引用  查看    

#7楼 2009-07-03 09:05 徐少侠      

@补丁
同样哈哈

不过对博主的文章还是要顶一下的

做学问有两种

1、努力学习正统的编码风格以及原则,努力做到自己的代码都在可知或合理的范围内运行,这辈子都不会碰到任何奇怪的问题

2、努力挖掘各种角落的东西,类似博主的这种东西。

其实,正统程序员就是1,而黑客等等的就是2

两个有点像,却又不一样

说谁更好?不一定
  回复  引用  查看    

#8楼 2009-07-03 09:14 Da Vinci      

这是编译器的事情。只知道C++中一定会执行,因为全局展开了。   回复  引用  查看    

#9楼 2009-07-03 09:15 diggingdeeply_马甲[未注册用户]

@韦恩卑鄙
我这是严谨说法,怕给人留下缝子喷我。

@徐少侠
扫清每个角落,不留盲点。



早上匆匆成文,可能存在不足之处,欢迎大家指正。
  回复  引用    

#10楼 2009-07-03 09:25 Jeffrey Zhao      

--引用--------------------------------------------------
@韦恩卑鄙: @韦恩卑鄙
如果是服务器端程序,那可不是50个线程能解决问题的。
--------------------------------------------------------
50个线程不止,这没错,不过如果200个甚至500个你还嫌不够,那一般就是程序的问题了。
  回复  引用  查看    

#11楼 2009-07-03 09:31 DiryBoy      

貌似旧版的能捕捉到 StackOverflowException,3.5就捕捉不到了。   回复  引用  查看    

#12楼 2009-07-03 09:32 Jeffrey Zhao      

@DiryBoy
有时捕捉得到,有时捕捉不到。
即使捕捉不到,也很难修复。
所以StackOverflowException是不管咋样都不应该让它出现的。
  回复  引用  查看    

#13楼 2009-07-03 09:33 不及格的程序员-八神      

从 .NET Framework 2.0 版开始,将无法通过 try-catch 块捕获 StackOverflowException 对象,并且默认情况下将终止相应的进程。   回复  引用  查看    

#14楼 2009-07-03 09:59 路人马甲[未注册用户]

进来受教的   回复  引用    

#15楼 2009-07-03 10:08 路人123[未注册用户]

拔电源那哥们太彪悍了,超人!!!哈哈。能在那准确的一瞬间让CPU停运!!!   回复  引用    

#16楼 2009-07-03 10:28 Da Vinci      

@路人123
就算拔电源,CPU也不是瞬间停运好不好!
  回复  引用  查看    

#17楼 2009-07-03 10:56 韦恩卑鄙      

--引用--------------------------------------------------
@韦恩卑鄙: @韦恩卑鄙
如果是服务器端程序,那可不是50个线程能解决问题的。

楼主你说说如何修改线程的堆栈的默认大小。我需要改的小一下,节省一些内存?
--------------------------------------------------------
线城池 完成端口 异步 而不是60 个等待的线程

200个人再线的时候 我上一游戏大厅大概40个线程
  回复  引用  查看    

#18楼 2009-07-03 11:03 缘清(aicken)      

finally与catch运行在不同的线程中,即使宿主AppDomain溢出,从技术角度来讲finally也会运行,但是这种运行已经没有意义。   回复  引用  查看    

#19楼[楼主] 2009-07-03 11:14 DiggingDeeply      

@缘清(aicken)
我也听过这种说法,但是没有证据说明是运行在不同的线程里。
请帮忙找找资料,谢谢!

还有我有一个反推的想法,如果按照这个说法,每当try。。。catch。。。finally会分线程执行的话,那深层嵌套的try。。。catch。。。finally岂不是性能很差?再用在多线程的程序里那就是恶梦了。
我在调试的过程中确实发现代码的执行不是顺序的,我猜原因可能有二:
1。如你所说,分开线程执行
2。JIT能动态改变执行的顺序(仅是本人猜测)

我有时间好好看看rotor,找找证据。
  回复  引用  查看    

#20楼 2009-07-03 11:18 过路[未注册用户]

@devil0153

您说,google了半天发现StackOverflow是捕获不到的
我写了下面的代码测试。
.net framework ver:3.5

===================
protected void Page_Load(object sender, EventArgs e)
{
try
{
throw new StackOverflowException();
}
catch (Exception ex)
{
Response.Write(ex.Message);
}
finally
{
Response.Write("finally");
}
}
================================
结果执行一切正常。
  回复  引用    

#21楼[楼主] 2009-07-03 11:23 DiggingDeeply      

@韦恩卑鄙
记得在《windows核心编程》里说过在创建线程时,可以制定栈的最大值。
在C# 里,就是在http://msdn.microsoft.com/zh-cn/library/system.threading.thread.thread.aspx

Thread 构造函数 (ThreadStart, Int32)

更新:2007 年 11 月

初始化 Thread 类的新实例,指定线程的最大堆栈大小。
  回复  引用  查看    

#22楼[楼主] 2009-07-03 11:25 DiggingDeeply      

@过路
这是user code抛出来的, 是假的溢出,可以catch。
而真的溢出时是CLR unhandle的,导致了CLR崩溃。
  回复  引用  查看    

#23楼 2009-07-03 13:13 GUO Xingwang      

如果你非得较真用P/V Invoke来创建内核级线程

不太懂 楼主可以解释下吗? 为什么叫内核级线程呢?
  回复  引用  查看    

#24楼[楼主] 2009-07-03 14:47 DiggingDeeply      

@GUO Xingwang
线程分两种,用户态和内核态,前者运行在os内存的用户区,后者运行在内核。
  回复  引用  查看    

#25楼 2009-07-03 15:59 Jeffrey Zhao      

@DiggingDeeply
用户态的线程一般是指fiber吧。
我看过Concurrent Programming on Windows,托管Thread对象其实就是一个Native Thread的封装,只是在托管部分补充了一些数据,可以让CLR进行识别和操作而已。
  回复  引用  查看    

#26楼 2009-07-03 16:12 凌军      

不错,没这么想过。受教了。   回复  引用  查看    

#27楼 2009-07-03 16:36 小猴子      

我说下自己的见解:
1 Finally 的执行
对于程序出现的严重异常,而不处理,系统会直接关闭的程序,与程序的内部执行机制是相对独立的。
毕竟应用程序是工作在操作系统的基础上,而操作系统是工作上硬件控制程序上的。那这个事情说是TRY finally的问题,是不可取的!就如上面的一个朋友,直接POWER OF一样。那不是一个层次的问题!
2 WINDOWS系统的THREAD数量
简单的打开下任务管理器,也没有发现使用THREAD数量多的程序就占用内存多啊?如果WINDWOS系统只能运行2048个THREAD,那也太低估了点。我前段时间写的小程序,在一台机器上运行100个客户端加服务端程序,总THREAD数量就超过了500了,他们总的内存还不到300M,服务端有超过300THREAD,而使用的内存不超过30M,算上虚拟内存也不超过400M(具体多少,还真忘记了)。
  回复  引用  查看    

#28楼 2009-07-03 16:46 diggingdeeply_马甲[未注册用户]

@Jeffrey Zhao
好像不是一个东西,filber好像比thread更小。(猜测,类似于一个job,结构上应该比thread简单,所以开销小)
对,可以用windbg将进程和线程的结构dump出来观察一下,我回去找找,以前观察过。

  回复  引用    

#29楼 2009-07-03 17:02 diggingdeeply_马甲[未注册用户]

@小猴子
1。我就是想知道是什么机制保证了finally的肯定执行?如果你知道,请不吝告知。
2。对于每个进程来说,只能看到虚存,最大是4G,但是2G保留给内核,所以实际上你用户态的程序最多就2G内存使用。所以2G/1M=2048。

我刚看了一下,thread最小最大值是256k,所以还能更多。
  回复  引用    

#30楼 2009-07-03 17:07 不多贱      

线程接触不多,学习了...   回复  引用  查看    

#31楼 2009-07-03 17:15 小猴子      

@diggingdeeply_马甲
--引用--------------------------------------------------
diggingdeeply_马甲: @小猴子
1。我就是想知道是什么机制保证了finally的肯定执行?如果你知道,请不吝告知。
2。对于每个进程来说,只能看到虚存,最大是4G,但是2G保留给内核,所以实际上你用户态的程序最多就2G内存使用。所以2G/1M=2048。

我刚看了一下,thread最小最大值是256k,所以还能更多。
--------------------------------------------------------
第一个问题:在目前的机制下,我认为能在汇编下找到FINALLY被执行的机制
第二个问题:THREAD的数量是会比较多,这个就和WINDOWS内核的句柄密切相关了,建议去看看WINDOWS系统原理,能解决一些疑惑。
我不过是一知半解,如果说错了,请不要介意,直接指出来就OK了。
  回复  引用  查看    

#32楼 2009-07-03 17:27 diggingdeeply_马甲[未注册用户]

@小猴子
1我看了汇编了,但是由于中间有JIT ,根本看不明白。
2。你说的我没看明白。线程是隶属于进程的,所以os里很多线程也没什么,平均起来每个进程也没几个。

介意什么啊,我也不懂,才发帖出来问。
  回复  引用    

#33楼 2009-07-03 17:36 小猴子      

呵呵!先就这样了,下班了。   回复  引用  查看    

#34楼 2009-07-03 18:42 Jeffrey Zhao      

@diggingdeeply_马甲
我是说,提到“用户态线程”一般是指“Fiber”,而不是现在所说的“线程”,Managed Thread还是需要进入Kernal Mode的。
Fiber在.net里无法直接使用,是个遗憾。
  回复  引用  查看    

#35楼[楼主] 2009-07-03 19:55 DiggingDeeply      

@Jeffrey Zhao
可以 P/V invoke 吧。
  回复  引用  查看    

#36楼 2009-07-04 02:55 Ivony...

看完了,与finally有什么关系?

StackOverflowException是不能被捕获的,在类库参考里面写的很清楚:

无法通过 try-catch 块捕获引发的 StackOverflowException。因此,该异常将导致该进程立即终止。
  回复  引用