一:背景
1. 讲故事
在项目中摸爬滚打几年,应该或多或少的见过有人把异常当做业务逻辑处理的情况(┬_┬),比如说判断一个数字是否为整数,就想当然的用try catch
包起来,再进行 int.Parse
,如果抛异常就说明不是整数,简单粗暴,也不需要写正则或者其他逻辑,再比如一个字符串强制转化为Enum,直接用Enum.Parse
,可能是因为对异常的开销不是特别了解,这种不好的使用习惯也许被官方发现了,后续给我们补了很多的Try前缀的方法,比如:int.TryParse
, Enum.TryParse
, dict.TryGetValue
,用代码展示如下:
//原始写法 var num = int.Parse("1"); //使用try方式 var result = 0; var b = int.TryParse("1", out result);
用Try系列方法没毛病,但这写法让人吐槽,还要单独定义result变量,没撤,官方还得靠我们这些开发者给他们发扬光大????????????,终于在C# 7.0 中新增了一个 out variables
语法糖。
//try out 变量模式 var c = int.TryParse("1", out int result2);
这种 out 变量
模式就????????了,一个方法获取两个值,还没有抛异常的风险。
二:为什么要用tryxxx方法
有了tryxxx方法之后,你就应该明白微软已经在提醒我们开发人员不要滥用异常,尤其在可预知可预见的场景下,毕竟他们知道异常的开销真的是太大了,不知者不怪哈。
1. 肉眼看得见的低性能
为了让大家肉眼能看见,我们就用异常方法和tryxxx方法做一个性能比较,迭代50w次,看看各自的性能如何?
for (int i = 0; i < 3; i++) { var watch = Stopwatch.StartNew(); for (int k = 0; k < 50000; k++) { try { var num = int.Parse("xxx"); } catch (Exception ex) { } } watch.Stop(); Console.WriteLine($"i={i + 1},耗费:{watch.ElapsedMilliseconds}"); } Console.WriteLine("---------------------------------------------"); for (int i = 0; i < 3; i++) { var watch = Stopwatch.StartNew(); for (int k = 0; k < 50000; k++) { var num = int.TryParse("xxx", out int reuslt); } watch.Stop(); Console.WriteLine($"i={i + 1},耗费:{watch.ElapsedMilliseconds}"); } Console.ReadLine();
看结果还挺吓人的,相差480倍, 好熟悉的一个数字。。。 南朝四百八十寺,多少楼台烟雨中
????????????
三: 异常的超强开销
为什么异常有那么大的开销? 只有知己知彼才能心中有数,看过我多线程视频的朋友应该知道,线程的创建和销毁代价都是非常大的,其中有一项就是需要代码从用户态切换到了内核态,毕竟线程是操作系统层面的事情,和你CLR无关,CLR只是做了一层系统包装而已,其实很多人都想不到,我们用的 try catch finally
底层也是封装了操作系统层面的(Windows 结构化异常处理),也叫做SEH,什么意思? 就是当你throw之后,代码需要从用户态切换到内核态,这个开销是不会小的,还有一个开销来自于Exception中的StackTrace,这里面的值需要从当前异常的线程栈中去抓取调用堆栈,栈越深,开销就越大。
1. 从用户态到内核态
大家肯定会说,甭那么玄乎,凡事都要讲个证据, Do more,Talk less
, 这里我准备分两种情况讲解。
<1> 有catch情况
准备在catch的时候阻塞住,然后抓它的dump文件。
public static void Main(string[] args) { try { var num = int.Parse("xxx"); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.ReadLine(); } }
使用 !dumpstack
把当前 0号线程 的所有托管和非托管堆栈全部打出来,简化后如下:
0:000> ~0sntdll!NtReadFile+0x14:00007fff`f805aa64 c3 ret0:000> !dumpstackOS Thread Id: 0x2bf0 (0)Current frame: ntdll!NtReadFile+0x14Caller, Callee(MethodDesc 00007fffde3a40b8 +0x18 System.Console.ReadLine())(MethodDesc 00007fff810d59f8 +0xa5 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3a40b8 +0 System.Console.ReadLine())00000044433fc700 00007fffe07a29e0 clr!ExceptionTracker::CallCatchHandler+0x9c, calling clr!ExceptionTracker::CallHandlerclr!ClrUnwindEx+0x40, calling ntdll!RtlUnwindExntdll!RtlRaiseException+0x4e, calling ntdll!RtlpCaptureContextclr!IL_Throw+0x114, calling clr!RaiseTheExceptionInternalOnly(MethodDesc 00007fffde4f95c0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)), calling mscorlib_ni+0x53976a(MethodDesc 00007fffde3b5330 +0xae System.Number.ParseInt32(System.String, System.Globalization.NumberStyles, System.Globalization.NumberFormatInfo)), calling (MethodDesc 00007fffde4f95c0 +0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean))(MethodDesc 00007fffde1ebfa8 +0x2eb System.Globalization.NumberFormatInfo..ctor(System.Globalization.CultureData)), calling (MethodDesc 00007fffde1eba68 +0 System.Globalization.CultureData.GetNFIValues(System.Globalization.NumberFormatInfo))(MethodDesc 00007fff810d59f8 +0x49 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3b1708 +0 System.Int32.Parse(System.String))
因为是堆栈,所以执行流就要从后往前看,你会发现流程大概是这个样子 int.Parse -> CLR -> ntdll -> CLR -> Console.ReadLine
,很显然 ntdll.dll 是操作系统层级的一个核心文件,这就从用户态切入到了内核态,如果不是很明白,我画一张简图吧。。。
<2>. 无catch处理
大家肯定很好奇,如果无catch会是怎么样,大家也可以用windbg去挖一下。
public static void Main(string[] args) { var num = int.Parse("xxx"); }0:000> !dumpstackOS Thread Id: 0xd68 (0)Current frame: ntdll!NtTerminateProcess+0x14Caller, Calleemscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x285, calling KERNEL32!ExitProcessImplementationmscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14, calling mscoreei!RuntimeDesc::ShutdownAllActiveRuntimesclr!EEPolicy::ExitProcessViaShim+0x9cclr!SafeExitProcess+0x9d, calling clr!EEPolicy::ExitProcessViaShimntdll!KiUserExceptionDispatch+0x53, calling ntdll!NtRaiseExceptionclr!RaiseTheExceptionInternalOnly+0x188426, calling clr!EEPolicy::HandleFatalErrorclr!IL_Throw+0x45, calling clr!LazyMachStateCaptureState(MethodDesc 00007fffde4f95c0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)), calling mscorlib_ni+0x53976a(MethodDesc 00007fffde3b5330 +0xae System.Number.ParseInt32(System.String, System.Globalization.NumberStyles, System.Globalization.NumberFormatInfo)), calling (MethodDesc 00007fffde4f95c0 +0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean))(MethodDesc 00007fffde1ebfa8 +0x2eb System.Globalization.NumberFormatInfo..ctor(System.Globalization.CultureData)), calling (MethodDesc 00007fffde1eba68 +0 System.Globalization.CultureData.GetNFIValues(System.Globalization.NumberFormatInfo))(MethodDesc 00007fff810e59f8 +0x37 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3b1708 +0 System.Int32.Parse(System.String))
可以看到进程的退出逻辑给了托管程序入口 mscoreei.dll
而再也没有进入Main函数了, 为此我也补一张图给大家看看
2. 抓取线程调用栈
当大家慌慌张张的看到异常的时候,第一眼会去看异常信息是什么? 第二眼会去看异常出在了哪一行代码,这就是线程的调用栈,这个信息非常重要,可以快捷的帮助我们找到问题解决问题,放在Exception的StackTrace中,先上一段代码。
public static void Main(string[] args) { Run(); Console.ReadLine(); } public static void Run() { var ex = new FormatException("你的格式错误啦!!!"); throw ex; }
<1> StackTrace何时塞入的
到目前为止还没看到哪本书说到StackTrace是何时被塞入的? 由于水平有限,我也试着探测一下下。
从代码中可以看到不是在new的时候塞入的,那会是哪里呢?
<2> 从CLR中寻找答案
既然不在用户代码,那就到CLR中去看看,在windbg中用 dumpstack
去查看非托管堆栈。
0:000> !dumpstackOS Thread Id: 0x4090 (0)Current frame: ntdll!NtTerminateProcess+0x14Caller, Calleeclr!EETypeHashTable::FindItem+0x532, calling clr!NgenHashTable<EEClassHashTable,EEClassHashEntry,4>::PersistedBucketList::GetBucketclr!JIT_StrCns+0xd0, calling clr!HelperMethodFrameRestoreState(MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throwclr!IL_Throw+0x45, calling clr!LazyMachStateCaptureState(MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throw(MethodDesc 00007fff810f59f8 +0x28 ConsoleApp4.Program.Main(System.String[])), calling 00007fff81200488 (stub for ConsoleApp4.Program.Run())
从简化后的流程看,怀疑是由 clr!HelperMethodFrameRestoreState
处理的,为什么这么说呢? 因为我们定义的 FormatException ex
会传给CLR的,不信可以用 kb
看一看。
0:000> kb # RetAddr : Args to Child : Call Site00 00007fff`e07a3181 : 00000000`e0434352 0000006d`4a7fe938 0000017b`30ad2d48 0000017b`2f081690 : KERNELBASE!RaiseException+0x6801 00007fff`e07a45f4 : ffffffff`fffffffe 0000017b`2ef02542 00000000`0000000a 0000017b`2f040910 : clr!RaiseTheExceptionInternalOnly+0x31f02 00007fff`811d0950 : 00000000`70000001 00007fff`810c4140 0000006d`4a7fedb8 0000006d`4a7fec78 : clr!IL_Throw+0x11403 00007fff`811d08b8 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d095004 00007fff`e0736c93 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d08b805 00007fff`e0736b79 : 00000000`00000000 00007fff`e0737aae 0000006d`4a7fefb8 00000000`00000000 : clr!CallDescrWorkerInternal+0x8306 00007fff`e0737410 : 0000006d`4a7fefb8 0000006d`4a7ff048 0000006d`4a7feeb8 00000000`00000001 : clr!CallDescrWorkerWithHandler+0x4e07 00007fff`e08dcaf2 : 0000006d`4a7fee00 00000000`00000001 00000000`00000001 0000017b`2efcecf0 : clr!MethodDescCallSite::CallTargetWorker+0x10208 00007fff`e08dd4b3 : 00000000`00000001 00000000`00000000 0000017b`30ad2d30 0000017b`30ad2d30 : clr!RunMain+0x25f09 00007fff`e08dd367 : 0000017b`2f040910 0000006d`4a7ff420 0000017b`2f040910 0000017b`2f082770 : clr!Assembly::ExecuteMainMethod+0xb70a 00007fff`e08dccb3 : 00000000`00000000 0000017b`2ef00000 00000000`00000000 00000000`00000000 : clr!SystemDomain::ExecuteMainMethod+0x6430b 00007fff`e08dcc31 : 0000017b`2ef00000 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!ExecuteEXE+0x3f0c 00007fff`e08de0a4 : ffffffff`ffffffff 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0xb20d 00007fff`e1208a61 : 00000000`00000000 00007fff`00000091 00000000`00000000 0000006d`4a7ff9f8 : clr!CorExeMain+0x140e 00007fff`e133a4cc : 00000000`00000000 00007fff`e08de090 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x1120f 00007fff`f5cc4034 : 00007fff`e1200000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x6c10 00007fff`f8033691 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x1411 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
其中第一行的 00 00007fff
e07a3181 : 00000000e0434352 0000006d
4a7fe938 0000017b30ad2d48 0000017b
2f081690 : KERNELBASE!RaiseException+0x68中的第三个参数地址
0000017b30ad2d48` 就是我们的异常类,打印出来看一下。
0:000> !do 0000017b30ad2d48Name: System.FormatExceptionMethodTable: 00007fffde285c38EEClass: 00007fffde3930e0Size: 160(0xa0) bytesFile: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dllFields: MT Field Offset Type VT Attr Value Name00007fffde2059c0 40002a2 8 System.String 0 instance 0000017b30ad4c80 _className00007fffde282a50 40002a3 10 ...ection.MethodBase 0 instance 0000000000000000 _exceptionMethod00007fffde2059c0 40002a4 18 System.String 0 instance 0000000000000000 _exceptionMethodString00007fffde2059c0 40002a5 20 System.String 0 instance 0000017b30ad2de8 _message00007fffde2883d8 40002a6 28 ...tions.IDictionary 0 instance 0000000000000000 _data00007fffde205b70 40002a7 30 System.Exception 0 instance 0000000000000000 _innerException00007fffde2059c0 40002a8 38 System.String 0 instance 0000000000000000 _helpURL00007fffde205dd8 40002a9 40 System.Object 0 instance 0000017b30ad2e98 _stackTrace00007fffde205dd8 40002aa 48 System.Object 0 instance 0000017b30ad2f28 _watsonBuckets00007fffde2059c0 40002ab 50 System.String 0 instance 0000000000000000 _stackTraceString00007fffde2059c0 40002ac 58 System.String 0 instance 0000000000000000 _remoteStackTraceString00007fffde2085a0 40002ad 88 System.Int32 1 instance 0 _remoteStackIndex00007fffde205dd8 40002ae 60 System.Object 0 instance 0000000000000000 _dynamicMethods00007fffde2085a0 40002af 8c System.Int32 1 instance -2146233033 _HResult00007fffde2059c0 40002b0 68 System.String 0 instance 0000000000000000 _source00007fffde2831f8 40002b1 78 System.IntPtr 1 instance 0 _xptrs00007fffde2085a0 40002b2 90 System.Int32 1 instance -532462766 _xcode00007fffde21e720 40002b3 80 System.UIntPtr 1 instance 0 _ipForWatsonBuckets00007fffde1f5080 40002b4 70 ...ializationManager 0 instance 0000017b30ad2e18 _safeSerializationManager00007fffde205dd8 40002a1 100 System.Object 0 shared static s_EDILock >> Domain:Value 0000017b2efe0af0:NotInit <<0:000> !do 0000017b30ad2e98Name: System.SByte[]MethodTable: 00007fffde20dde8EEClass: 00007fffde390920Size: 120(0x78) bytesArray: Rank 1, Number of elements 96, Type SByte (Print Array)Content: .........../{...P.......@..Jm....Z.........................Jm....Y..............................Fields:None
此时 _stackTrace
已经有值了,毕竟Console上已经打印出来了。
最后补充一下大家也可以通过 !threads
去找异常的线程,如下图的中 System.FormatException 0000017b30ad2d48
,然后通过 !printexception
去打印这个地址 0000017b30ad2d48
上异常对象。
0:000> !threadsThreadCount: 2UnstartedThread: 0BackgroundThread: 1PendingThread: 0DeadThread: 0Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 80c 0000016816f508f0 2a020 Preemptive 0000016818CCE3B8:0000016818CCFFD0 0000016816ef0b10 0 MTA System.FormatException 0000017b30ad2d48 6 2 12d8 0000016816f7b0e0 2b220 Preemptive 0000000000000000:0000000000000000 0000016816ef0b10 0 MTA (Finalizer) 0:000> !printexception 0000017b30ad2d48Exception object: 0000017b30ad2d48Exception type: System.FormatExceptionMessage: 你的格式错误啦!!!InnerException: <none>StackTrace (generated): SP IP Function 0000001F8F7FEE90 00007FFF811E0951 ConsoleApp4!ConsoleApp4.Program.Run()+0x71 0000001F8F7FEEE0 00007FFF811E08B9 ConsoleApp4!ConsoleApp4.Program.Main(System.String[])+0x29StackTraceString: <none>HResult: 80131537
三:总结
不要把异常当做业务逻辑处理,这开销有可能你承受不起,把那些真正不可期的情况留给异常吧,如: TimeoutException。。。
---------------------
作者:一线码农