51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 814|回复: 0
打印 上一主题 下一主题

测试你无法承受的这些事!

[复制链接]
  • TA的每日心情
    无聊
    2024-11-5 10:03
  • 签到天数: 77 天

    连续签到: 1 天

    [LV.6]测试旅长

    跳转到指定楼层
    1#
    发表于 2022-9-19 13:08:03 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    一:背景1. 讲故事

    在项目中摸爬滚打几年,应该或多或少的见过有人把异常当做业务逻辑处理的情况(┬_┬),比如说判断一个数字是否为整数,就想当然的用try catch 包起来,再进行 int.Parse,如果抛异常就说明不是整数,简单粗暴,也不需要写正则或者其他逻辑,再比如一个字符串强制转化为Enum,直接用Enum.Parse,可能是因为对异常的开销不是特别了解,这种不好的使用习惯也许被官方发现了,后续给我们补了很多的Try前缀的方法,比如:int.TryParse , Enum.TryParse, dict.TryGetValue ,用代码展示如下:

    1. <font size="3">            //原始写法
    2.             var num = int.Parse("1");

    3.             //使用try方式
    4.             var result = 0;
    5.             var b = int.TryParse("1", out result);
    6. </font>
    复制代码

    用Try系列方法没毛病,但这写法让人吐槽,还要单独定义result变量,没撤,官方还得靠我们这些开发者给他们发扬光大,终于在C# 7.0 中新增了一个 out variables 语法糖。

    1. <font size="3">
    2.             //try out 变量模式
    3.             var c = int.TryParse("1", out int result2);

    4. </font>
    复制代码

    这种 out 变量 模式就牛了,一个方法获取两个值,还没有抛异常的风险。

    二:为什么要用tryxxx方法

    有了tryxxx方法之后,你就应该明白微软已经在提醒我们开发人员不要滥用异常,尤其在可预知可预见的场景下,毕竟他们知道异常的开销真的是太大了,不知者不怪哈。

    1. 肉眼看得见的低性能

    为了让大家肉眼能看见,我们就用异常方法和tryxxx方法做一个性能比较,迭代50w次,看看各自的性能如何?

    1. <font size="3">
    2.             for (int i = 0; i < 3; i++)
    3.             {
    4.                 var watch = Stopwatch.StartNew();
    5.                 for (int k = 0; k < 50000; k++)
    6.                 {
    7.                     try
    8.                     {
    9.                         var num = int.Parse("xxx");
    10.                     }
    11.                     catch (Exception ex) { }
    12.                 }
    13.                 watch.Stop();

    14.                 Console.WriteLine([        DISCUZ_CODE_127        ]quot;i={i + 1},耗费:{watch.ElapsedMilliseconds}");
    15.             }
    16.             Console.WriteLine("---------------------------------------------");
    17.             for (int i = 0; i < 3; i++)
    18.             {
    19.                 var watch = Stopwatch.StartNew();

    20.                 for (int k = 0; k < 50000; k++)
    21.                 {
    22.                    var num = int.TryParse("xxx", out int reuslt);
    23.                 }

    24.                 watch.Stop();

    25.                 Console.WriteLine([        DISCUZ_CODE_127        ]quot;i={i + 1},耗费:{watch.ElapsedMilliseconds}");
    26.             }
    27.             Console.ReadLine();

    28. </font>
    复制代码

    看结果还挺吓人的,相差480倍, 好熟悉的一个数字。。。 南朝四百八十寺,多少楼台烟雨中

    三: 异常的超强开销

    为什么异常有那么大的开销? 只有知己知彼才能心中有数,看过我多线程视频的朋友应该知道,线程的创建和销毁代价都是非常大的,其中有一项就是需要代码从用户态切换到了内核态,毕竟线程是操作系统层面的事情,和你CLR无关,CLR只是做了一层系统包装而已,其实很多人都想不到,我们用的 try catch finally 底层也是封装了操作系统层面的(Windows 结构化异常处理),也叫做SEH,什么意思? 就是当你throw之后,代码需要从用户态切换到内核态,这个开销是不会小的,还有一个开销来自于Exception中的StackTrace,这里面的值需要从当前异常的线程栈中去抓取调用堆栈,栈越深,开销就越大。

    1. 从用户态到内核态

    大家肯定会说,甭那么玄乎,凡事都要讲个证据, Do more,Talk less, 这里我准备分两种情况讲解。

    <1> 有catch情况

    准备在catch的时候阻塞住,然后抓它的dump文件。

    1. <font size="3">
    2.         public static void Main(string[] args)
    3.         {
    4.             try
    5.             {
    6.                 var num = int.Parse("xxx");
    7.             }
    8.             catch (Exception ex)
    9.             {
    10.                 Console.WriteLine(ex.Message);
    11.                 Console.ReadLine();
    12.             }
    13.         }

    14. </font>
    复制代码

    使用 !dumpstack 把当前 0号线程 的所有托管和非托管堆栈全部打出来,简化后如下:

    1. <font size="3">
    2. 0:000> ~0s
    3. ntdll!NtReadFile+0x14:
    4. 00007fff`f805aa64 c3              ret
    5. 0:000> !dumpstack
    6. OS Thread Id: 0x2bf0 (0)
    7. Current frame: ntdll!NtReadFile+0x14
    8. Caller, Callee
    9. (MethodDesc 00007fffde3a40b8 +0x18 System.Console.ReadLine())
    10. (MethodDesc 00007fff810d59f8 +0xa5 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3a40b8 +0 System.Console.ReadLine())
    11. 00000044433fc700 00007fffe07a29e0 clr!ExceptionTracker::CallCatchHandler+0x9c, calling clr!ExceptionTracker::CallHandler
    12. clr!ClrUnwindEx+0x40, calling ntdll!RtlUnwindEx
    13. ntdll!RtlRaiseException+0x4e, calling ntdll!RtlpCaptureContext
    14. clr!IL_Throw+0x114, calling clr!RaiseTheExceptionInternalOnly
    15. (MethodDesc 00007fffde4f95c0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)), calling mscorlib_ni+0x53976a
    16. (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))
    17. (MethodDesc 00007fffde1ebfa8 +0x2eb System.Globalization.NumberFormatInfo..ctor(System.Globalization.CultureData)), calling (MethodDesc 00007fffde1eba68 +0 System.Globalization.CultureData.GetNFIValues(System.Globalization.NumberFormatInfo))
    18. (MethodDesc 00007fff810d59f8 +0x49 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3b1708 +0 System.Int32.Parse(System.String))

    19. </font>
    复制代码

    因为是堆栈,所以执行流就要从后往前看,你会发现流程大概是这个样子 int.Parse -> CLR -> ntdll -> CLR -> Console.ReadLine,很显然 ntdll.dll 是操作系统层级的一个核心文件,这就从用户态切入到了内核态,如果不是很明白,我画一张简图吧。。。

    <2>. 无catch处理

    大家肯定很好奇,如果无catch会是怎么样,大家也可以用windbg去挖一下。

    1. <font size="3">
    2.         public static void Main(string[] args)
    3.         {
    4.             var num = int.Parse("xxx");
    5.         }


    6. 0:000> !dumpstack
    7. OS Thread Id: 0xd68 (0)
    8. Current frame: ntdll!NtTerminateProcess+0x14
    9. Caller, Callee
    10. mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x285, calling KERNEL32!ExitProcessImplementation
    11. mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14, calling mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes
    12. clr!EEPolicy::ExitProcessViaShim+0x9c
    13. clr!SafeExitProcess+0x9d, calling clr!EEPolicy::ExitProcessViaShim
    14. ntdll!KiUserExceptionDispatch+0x53, calling ntdll!NtRaiseException
    15. clr!RaiseTheExceptionInternalOnly+0x188426, calling clr!EEPolicy::HandleFatalError
    16. clr!IL_Throw+0x45, calling clr!LazyMachStateCaptureState
    17. (MethodDesc 00007fffde4f95c0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)), calling mscorlib_ni+0x53976a
    18. (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))
    19. (MethodDesc 00007fffde1ebfa8 +0x2eb System.Globalization.NumberFormatInfo..ctor(System.Globalization.CultureData)), calling (MethodDesc 00007fffde1eba68 +0 System.Globalization.CultureData.GetNFIValues(System.Globalization.NumberFormatInfo))
    20. (MethodDesc 00007fff810e59f8 +0x37 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3b1708 +0 System.Int32.Parse(System.String))

    21. </font>
    复制代码

    可以看到进程的退出逻辑给了托管程序入口 mscoreei.dll 而再也没有进入Main函数了, 为此我也补一张图给大家看看

    2. 抓取线程调用栈

    当大家慌慌张张的看到异常的时候,第一眼会去看异常信息是什么? 第二眼会去看异常出在了哪一行代码,这就是线程的调用栈,这个信息非常重要,可以快捷的帮助我们找到问题解决问题,放在Exception的StackTrace中,先上一段代码。

    1. <font size="3">
    2.     public static void Main(string[] args)
    3.         {
    4.             Run();
    5.             Console.ReadLine();
    6.         }

    7.         public static void Run()
    8.         {
    9.             var ex = new FormatException("你的格式错误啦!!!");
    10.             throw ex;
    11.         }

    12. </font>
    复制代码
    <1> StackTrace何时塞入的

    到目前为止还没看到哪本书说到StackTrace是何时被塞入的? 由于水平有限,我也试着探测一下下。

    从代码中可以看到不是在new的时候塞入的,那会是哪里呢?

    <2> 从CLR中寻找答案

    既然不在用户代码,那就到CLR中去看看,在windbg中用 dumpstack 去查看非托管堆栈。

    1. <font size="3">
    2. 0:000> !dumpstack
    3. OS Thread Id: 0x4090 (0)
    4. Current frame: ntdll!NtTerminateProcess+0x14
    5. Caller, Callee
    6. clr!EETypeHashTable::FindItem+0x532, calling clr!NgenHashTable<EEClassHashTable,EEClassHashEntry,4>::PersistedBucketList::GetBucket
    7. clr!JIT_StrCns+0xd0, calling clr!HelperMethodFrameRestoreState
    8. (MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throw
    9. clr!IL_Throw+0x45, calling clr!LazyMachStateCaptureState
    10. (MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throw
    11. (MethodDesc 00007fff810f59f8 +0x28 ConsoleApp4.Program.Main(System.String[])), calling 00007fff81200488 (stub for ConsoleApp4.Program.Run())

    12. </font>
    复制代码

    从简化后的流程看,怀疑是由 clr!HelperMethodFrameRestoreState 处理的,为什么这么说呢? 因为我们定义的 FormatException ex 会传给CLR的,不信可以用 kb 看一看。

    1. <font size="3">
    2. 0:000> kb
    3. # RetAddr           : Args to Child                                                           : Call Site
    4. 00 00007fff`e07a3181 : 00000000`e0434352 0000006d`4a7fe938 0000017b`30ad2d48 0000017b`2f081690 : KERNELBASE!RaiseException+0x68
    5. 01 00007fff`e07a45f4 : ffffffff`fffffffe 0000017b`2ef02542 00000000`0000000a 0000017b`2f040910 : clr!RaiseTheExceptionInternalOnly+0x31f
    6. 02 00007fff`811d0950 : 00000000`70000001 00007fff`810c4140 0000006d`4a7fedb8 0000006d`4a7fec78 : clr!IL_Throw+0x114
    7. 03 00007fff`811d08b8 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d0950
    8. 04 00007fff`e0736c93 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d08b8
    9. 05 00007fff`e0736b79 : 00000000`00000000 00007fff`e0737aae 0000006d`4a7fefb8 00000000`00000000 : clr!CallDescrWorkerInternal+0x83
    10. 06 00007fff`e0737410 : 0000006d`4a7fefb8 0000006d`4a7ff048 0000006d`4a7feeb8 00000000`00000001 : clr!CallDescrWorkerWithHandler+0x4e
    11. 07 00007fff`e08dcaf2 : 0000006d`4a7fee00 00000000`00000001 00000000`00000001 0000017b`2efcecf0 : clr!MethodDescCallSite::CallTargetWorker+0x102
    12. 08 00007fff`e08dd4b3 : 00000000`00000001 00000000`00000000 0000017b`30ad2d30 0000017b`30ad2d30 : clr!RunMain+0x25f
    13. 09 00007fff`e08dd367 : 0000017b`2f040910 0000006d`4a7ff420 0000017b`2f040910 0000017b`2f082770 : clr!Assembly::ExecuteMainMethod+0xb7
    14. 0a 00007fff`e08dccb3 : 00000000`00000000 0000017b`2ef00000 00000000`00000000 00000000`00000000 : clr!SystemDomain::ExecuteMainMethod+0x643
    15. 0b 00007fff`e08dcc31 : 0000017b`2ef00000 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!ExecuteEXE+0x3f
    16. 0c 00007fff`e08de0a4 : ffffffff`ffffffff 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0xb2
    17. 0d 00007fff`e1208a61 : 00000000`00000000 00007fff`00000091 00000000`00000000 0000006d`4a7ff9f8 : clr!CorExeMain+0x14
    18. 0e 00007fff`e133a4cc : 00000000`00000000 00007fff`e08de090 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
    19. 0f 00007fff`f5cc4034 : 00007fff`e1200000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x6c
    20. 10 00007fff`f8033691 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
    21. 11 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21


    22. </font>
    复制代码

    其中第一行的 00 00007fffe07a3181 : 00000000e0434352 0000006d4a7fe938 0000017b30ad2d48 0000017b2f081690 : KERNELBASE!RaiseException+0x68中的第三个参数地址 0000017b30ad2d48` 就是我们的异常类,打印出来看一下。

    1. <font size="3">
    2. 0:000> !do 0000017b30ad2d48
    3. Name:        System.FormatException
    4. MethodTable: 00007fffde285c38
    5. EEClass:     00007fffde3930e0
    6. Size:        160(0xa0) bytes
    7. File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    8. Fields:
    9.               MT    Field   Offset                 Type VT     Attr            Value Name
    10. 00007fffde2059c0  40002a2        8        System.String  0 instance 0000017b30ad4c80 _className
    11. 00007fffde282a50  40002a3       10 ...ection.MethodBase  0 instance 0000000000000000 _exceptionMethod
    12. 00007fffde2059c0  40002a4       18        System.String  0 instance 0000000000000000 _exceptionMethodString
    13. 00007fffde2059c0  40002a5       20        System.String  0 instance 0000017b30ad2de8 _message
    14. 00007fffde2883d8  40002a6       28 ...tions.IDictionary  0 instance 0000000000000000 _data
    15. 00007fffde205b70  40002a7       30     System.Exception  0 instance 0000000000000000 _innerException
    16. 00007fffde2059c0  40002a8       38        System.String  0 instance 0000000000000000 _helpURL
    17. 00007fffde205dd8  40002a9       40        System.Object  0 instance 0000017b30ad2e98 _stackTrace
    18. 00007fffde205dd8  40002aa       48        System.Object  0 instance 0000017b30ad2f28 _watsonBuckets
    19. 00007fffde2059c0  40002ab       50        System.String  0 instance 0000000000000000 _stackTraceString
    20. 00007fffde2059c0  40002ac       58        System.String  0 instance 0000000000000000 _remoteStackTraceString
    21. 00007fffde2085a0  40002ad       88         System.Int32  1 instance                0 _remoteStackIndex
    22. 00007fffde205dd8  40002ae       60        System.Object  0 instance 0000000000000000 _dynamicMethods
    23. 00007fffde2085a0  40002af       8c         System.Int32  1 instance      -2146233033 _HResult
    24. 00007fffde2059c0  40002b0       68        System.String  0 instance 0000000000000000 _source
    25. 00007fffde2831f8  40002b1       78        System.IntPtr  1 instance                0 _xptrs
    26. 00007fffde2085a0  40002b2       90         System.Int32  1 instance       -532462766 _xcode
    27. 00007fffde21e720  40002b3       80       System.UIntPtr  1 instance                0 _ipForWatsonBuckets
    28. 00007fffde1f5080  40002b4       70 ...ializationManager  0 instance 0000017b30ad2e18 _safeSerializationManager
    29. 00007fffde205dd8  40002a1      100        System.Object  0   shared           static s_EDILock
    30.                                  >> Domain:Value  0000017b2efe0af0:NotInit  <<

    31. 0:000> !do 0000017b30ad2e98
    32. Name:        System.SByte[]
    33. MethodTable: 00007fffde20dde8
    34. EEClass:     00007fffde390920
    35. Size:        120(0x78) bytes
    36. Array:       Rank 1, Number of elements 96, Type SByte (Print Array)
    37. Content:     .........../{...P.......@..Jm....Z.........................Jm....Y..............................
    38. Fields:
    39. None


    40. </font>
    复制代码

    此时 _stackTrace 已经有值了,毕竟Console上已经打印出来了。

    最后补充一下大家也可以通过 !threads 去找异常的线程,如下图的中 System.FormatException 0000017b30ad2d48,然后通过 !printexception 去打印这个地址 0000017b30ad2d48 上异常对象。

    1. <font size="3">
    2. 0:000> !threads
    3. ThreadCount:      2
    4. UnstartedThread:  0
    5. BackgroundThread: 1
    6. PendingThread:    0
    7. DeadThread:       0
    8. Hosted Runtime:   no
    9.                                                                                                         Lock  
    10.        ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
    11.    0    1  80c 0000016816f508f0    2a020 Preemptive  0000016818CCE3B8:0000016818CCFFD0 0000016816ef0b10 0     MTA System.FormatException 0000017b30ad2d48
    12.    6    2 12d8 0000016816f7b0e0    2b220 Preemptive  0000000000000000:0000000000000000 0000016816ef0b10 0     MTA (Finalizer)


    13. 0:000> !printexception 0000017b30ad2d48
    14. Exception object: 0000017b30ad2d48
    15. Exception type:   System.FormatException
    16. Message:          你的格式错误啦!!!
    17. InnerException:   <none>
    18. StackTrace (generated):
    19.     SP               IP               Function
    20.     0000001F8F7FEE90 00007FFF811E0951 ConsoleApp4!ConsoleApp4.Program.Run()+0x71
    21.     0000001F8F7FEEE0 00007FFF811E08B9 ConsoleApp4!ConsoleApp4.Program.Main(System.String[])+0x29

    22. StackTraceString: <none>
    23. HResult: 80131537

    24. </font>
    复制代码
    三:总结

    不要把异常当做业务逻辑处理,这开销有可能你承受不起,把那些真正不可期的情况留给异常吧,如: TimeoutException。。。


    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-23 17:56 , Processed in 0.071105 second(s), 24 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表