在項目中摸爬滾打幾年,應該或多或少的見過有人把異常當作業務邏輯處理的狀況(┬_┬),好比說判斷一個數字是否爲整數,就想固然的用try catch
包起來,再進行 int.Parse
,若是拋異常就說明不是整數,簡單粗暴,也不須要寫正則或者其餘邏輯,再好比一個字符串強制轉化爲Enum,直接用Enum.Parse
,多是由於對異常的開銷不是特別瞭解,這種很差的使用習慣也許被官方發現了,後續給咱們補了不少的Try前綴的方法,好比:int.TryParse
, Enum.TryParse
, dict.TryGetValue
,用代碼展現以下:xcode
//原始寫法 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 變量
模式就🐮👃了,一個方法獲取兩個值,尚未拋異常的風險。less
有了tryxxx方法以後,你就應該明白微軟已經在提醒咱們開發人員不要濫用異常,尤爲在可預知可預見的場景下,畢竟他們知道異常的開銷真的是太大了,不知者不怪哈。函數
爲了讓你們肉眼能看見,咱們就用異常方法和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倍, 好熟悉的一個數字。。。 南朝四百八十寺,多少樓臺煙雨中
😄😄😄spa
爲何異常有那麼大的開銷? 只有知己知彼才能心中有數,看過我多線程視頻的朋友應該知道,線程的建立和銷燬代價都是很是大的,其中有一項就是須要代碼從用戶態切換到了內核態,畢竟線程是操做系統層面的事情,和你CLR無關,CLR只是作了一層系統包裝而已,其實不少人都想不到,咱們用的 try catch finally
底層也是封裝了操做系統層面的(Windows 結構化異常處理),也叫作SEH,什麼意思? 就是當你throw以後,代碼須要從用戶態切換到內核態,這個開銷是不會小的,還有一個開銷來自於Exception中的StackTrace,這裏面的值須要從當前異常的線程棧中去抓取調用堆棧,棧越深,開銷就越大。操作系統
你們確定會說,甭那麼玄乎,凡事都要講個證據, Do more,Talk less
, 這裏我準備分兩種狀況講解。pwa
準備在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號線程 的全部託管和非託管堆棧所有打出來,簡化後以下:3d
0:000> ~0s ntdll!NtReadFile+0x14: 00007fff`f805aa64 c3 ret 0:000> !dumpstack OS Thread Id: 0x2bf0 (0) Current frame: ntdll!NtReadFile+0x14 Caller, 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::CallHandler clr!ClrUnwindEx+0x40, calling ntdll!RtlUnwindEx ntdll!RtlRaiseException+0x4e, calling ntdll!RtlpCaptureContext clr!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 是操做系統層級的一個核心文件,這就從用戶態切入到了內核態,若是不是很明白,我畫一張簡圖吧。。。
你們確定很好奇,若是無catch會是怎麼樣,你們也能夠用windbg去挖一下。
public static void Main(string[] args) { var num = int.Parse("xxx"); } 0:000> !dumpstack OS Thread Id: 0xd68 (0) Current frame: ntdll!NtTerminateProcess+0x14 Caller, Callee mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x285, calling KERNEL32!ExitProcessImplementation mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14, calling mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes clr!EEPolicy::ExitProcessViaShim+0x9c clr!SafeExitProcess+0x9d, calling clr!EEPolicy::ExitProcessViaShim ntdll!KiUserExceptionDispatch+0x53, calling ntdll!NtRaiseException clr!RaiseTheExceptionInternalOnly+0x188426, calling clr!EEPolicy::HandleFatalError clr!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函數了, 爲此我也補一張圖給你們看看
當你們慌慌張張的看到異常的時候,第一眼會去看異常信息是什麼? 第二眼會去看異常出在了哪一行代碼,這就是線程的調用棧,這個信息很是重要,能夠快捷的幫助咱們找到問題解決問題,放在Exception的StackTrace中,先上一段代碼。
public static void Main(string[] args) { Run(); Console.ReadLine(); } public static void Run() { var ex = new FormatException("你的格式錯誤啦!!!"); throw ex; }
到目前爲止還沒看到哪本書說到StackTrace是什麼時候被塞入的? 因爲水平有限,我也試着探測一下下。
從代碼中能夠看到不是在new的時候塞入的,那會是哪裏呢?
既然不在用戶代碼,那就到CLR中去看看,在windbg中用 dumpstack
去查看非託管堆棧。
0:000> !dumpstack OS Thread Id: 0x4090 (0) Current frame: ntdll!NtTerminateProcess+0x14 Caller, Callee clr!EETypeHashTable::FindItem+0x532, calling clr!NgenHashTable<EEClassHashTable,EEClassHashEntry,4>::PersistedBucketList::GetBucket clr!JIT_StrCns+0xd0, calling clr!HelperMethodFrameRestoreState (MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throw clr!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 Site 00 00007fff`e07a3181 : 00000000`e0434352 0000006d`4a7fe938 0000017b`30ad2d48 0000017b`2f081690 : KERNELBASE!RaiseException+0x68 01 00007fff`e07a45f4 : ffffffff`fffffffe 0000017b`2ef02542 00000000`0000000a 0000017b`2f040910 : clr!RaiseTheExceptionInternalOnly+0x31f 02 00007fff`811d0950 : 00000000`70000001 00007fff`810c4140 0000006d`4a7fedb8 0000006d`4a7fec78 : clr!IL_Throw+0x114 03 00007fff`811d08b8 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d0950 04 00007fff`e0736c93 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d08b8 05 00007fff`e0736b79 : 00000000`00000000 00007fff`e0737aae 0000006d`4a7fefb8 00000000`00000000 : clr!CallDescrWorkerInternal+0x83 06 00007fff`e0737410 : 0000006d`4a7fefb8 0000006d`4a7ff048 0000006d`4a7feeb8 00000000`00000001 : clr!CallDescrWorkerWithHandler+0x4e 07 00007fff`e08dcaf2 : 0000006d`4a7fee00 00000000`00000001 00000000`00000001 0000017b`2efcecf0 : clr!MethodDescCallSite::CallTargetWorker+0x102 08 00007fff`e08dd4b3 : 00000000`00000001 00000000`00000000 0000017b`30ad2d30 0000017b`30ad2d30 : clr!RunMain+0x25f 09 00007fff`e08dd367 : 0000017b`2f040910 0000006d`4a7ff420 0000017b`2f040910 0000017b`2f082770 : clr!Assembly::ExecuteMainMethod+0xb7 0a 00007fff`e08dccb3 : 00000000`00000000 0000017b`2ef00000 00000000`00000000 00000000`00000000 : clr!SystemDomain::ExecuteMainMethod+0x643 0b 00007fff`e08dcc31 : 0000017b`2ef00000 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!ExecuteEXE+0x3f 0c 00007fff`e08de0a4 : ffffffff`ffffffff 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0xb2 0d 00007fff`e1208a61 : 00000000`00000000 00007fff`00000091 00000000`00000000 0000006d`4a7ff9f8 : clr!CorExeMain+0x14 0e 00007fff`e133a4cc : 00000000`00000000 00007fff`e08de090 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112 0f 00007fff`f5cc4034 : 00007fff`e1200000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x6c 10 00007fff`f8033691 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14 11 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 0000017b30ad2d48 Name: System.FormatException MethodTable: 00007fffde285c38 EEClass: 00007fffde3930e0 Size: 160(0xa0) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007fffde2059c0 40002a2 8 System.String 0 instance 0000017b30ad4c80 _className 00007fffde282a50 40002a3 10 ...ection.MethodBase 0 instance 0000000000000000 _exceptionMethod 00007fffde2059c0 40002a4 18 System.String 0 instance 0000000000000000 _exceptionMethodString 00007fffde2059c0 40002a5 20 System.String 0 instance 0000017b30ad2de8 _message 00007fffde2883d8 40002a6 28 ...tions.IDictionary 0 instance 0000000000000000 _data 00007fffde205b70 40002a7 30 System.Exception 0 instance 0000000000000000 _innerException 00007fffde2059c0 40002a8 38 System.String 0 instance 0000000000000000 _helpURL 00007fffde205dd8 40002a9 40 System.Object 0 instance 0000017b30ad2e98 _stackTrace 00007fffde205dd8 40002aa 48 System.Object 0 instance 0000017b30ad2f28 _watsonBuckets 00007fffde2059c0 40002ab 50 System.String 0 instance 0000000000000000 _stackTraceString 00007fffde2059c0 40002ac 58 System.String 0 instance 0000000000000000 _remoteStackTraceString 00007fffde2085a0 40002ad 88 System.Int32 1 instance 0 _remoteStackIndex 00007fffde205dd8 40002ae 60 System.Object 0 instance 0000000000000000 _dynamicMethods 00007fffde2085a0 40002af 8c System.Int32 1 instance -2146233033 _HResult 00007fffde2059c0 40002b0 68 System.String 0 instance 0000000000000000 _source 00007fffde2831f8 40002b1 78 System.IntPtr 1 instance 0 _xptrs 00007fffde2085a0 40002b2 90 System.Int32 1 instance -532462766 _xcode 00007fffde21e720 40002b3 80 System.UIntPtr 1 instance 0 _ipForWatsonBuckets 00007fffde1f5080 40002b4 70 ...ializationManager 0 instance 0000017b30ad2e18 _safeSerializationManager 00007fffde205dd8 40002a1 100 System.Object 0 shared static s_EDILock >> Domain:Value 0000017b2efe0af0:NotInit << 0:000> !do 0000017b30ad2e98 Name: System.SByte[] MethodTable: 00007fffde20dde8 EEClass: 00007fffde390920 Size: 120(0x78) bytes Array: 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> !threads ThreadCount: 2 UnstartedThread: 0 BackgroundThread: 1 PendingThread: 0 DeadThread: 0 Hosted 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 0000017b30ad2d48 Exception object: 0000017b30ad2d48 Exception type: System.FormatException Message: 你的格式錯誤啦!!! InnerException: <none> StackTrace (generated): SP IP Function 0000001F8F7FEE90 00007FFF811E0951 ConsoleApp4!ConsoleApp4.Program.Run()+0x71 0000001F8F7FEEE0 00007FFF811E08B9 ConsoleApp4!ConsoleApp4.Program.Main(System.String[])+0x29 StackTraceString: <none> HResult: 80131537
不要把異常當作業務邏輯處理,這開銷有可能你承受不起,把那些真正不可期的狀況留給異常吧,如: TimeoutException。。。