• Net 高级调试之十四:线程同步的基础知识和常见的同步原语


    一、介绍
        今天是《Net 高级调试》的第十四篇文章,这篇文章我们主要介绍和线程相关的内容,当然不是教你如何去写多线程,更不会介绍多线程的使用方法和API,今天,我们主要讲一下锁,一说到多线程,就会有并发的问题,也可以说是线程安全的问题,锁是没有办法避开的一个话题。我们今天不讲锁的使用方法,主要是关注锁的底层实现原理,是如何实现的,让我们做到知其一,也要知其二,这些是 Net 框架的底层,了解更深,对于我们调试更有利。当然了,第一次看视频或者看书,是很迷糊的,不知道如何操作,还是那句老话,一遍不行,那就再来一遍,还不行,那就再来一遍,俗话说的好,书读千遍,其意自现。
         如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

           调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
              操作系统:Windows Professional 10
              调试工具:Windbg Preview(可以去Microsoft Store 去下载)
              开发工具:Visual Studio 2022
              Net 版本:Net Framework 4.8
              CoreCLR源码:源码下载

    二、基础知识
        1、线程同步原语
            1.1、C# Thread 的表示。
                我们在C# 程序中书写一个 Thread 线程类,其实,在背后会做很多事情,比如在 CLR 层会有一个对应的线程类生成,同时操作系统层也会有一个数据结构与之对应,所以说,我们简简单单声明一个 Thread 类,会有三个数据结构来承载。
                
                a)、C# 层的 Thread。
                    C# 中的 Thread 类,其实是对 CLR 层 Thread 线程类的封装,在 C# Thread 类的定义中,会有一个 private IntPtr DONT_USE_InternalThread 实例字段,该字段就是引用的 CLR 层的线程指针引用。

                b)、CLR 层的 Thread
                    Net Core 是开源的,所以是可以看到 CLR 线程 Thread 的定义。类名是:Thread.cpp,Net 5、6、7、8都可以看。

                c)、OS 层的 KThread。
                    操作系统层的线程对象是通过 _KThread 来表示的。

        2、事件原语
            2.1、AutoResetEvent 和 ManulResetEvent(内核锁)
                事件同步的本质实在内核态维护了一个 bool 值,通过 bool 值来实现线程间的同步,具体的使用方法网上很多,我这里就不过多的赘述了,这里我们看看是如何通过 bool 值的变化实现线程间的同步的。

            2.2、Semaphore(内核锁)
                AutoResetEvent、ManulResetEvent 维护的是 bool 类型的值,信号量本质上就是维护了一个 int 值,这就是两者的区别,我们可以使用 Windbg 来查看一下 waitHandle 的值,可以发现 Semaphore 的 Count 的值在不断的变化。

            2.3、Monitor(混合锁-内核锁)
               监视器是由 C# 中的 AwareLock 实现的,底层是基于 AutoResetEvent 机制,可以参见 coreclr 源码。因为 Monitor 是基于对象头的同步块索引来实现的,我们可以查看对象头的数据结构就可以明白了。
     
            2.4、ThinLock(用户态锁)
                【瘦锁】也是 CLR 基于【对象头】实现的一种轻量级的自旋锁,没有和内核态交互,所以性能非常高,这种实现的方式就是将【持有锁】线程的 Id 存在在对象头中,如果【对象头】中存不下就会转换成 Monitor 锁。

    三、源码调试
        废话不多说,这一节是具体的调试过程,又可以说是眼见为实的过程,在开始之前,我还是要啰嗦两句,这一节分为两个部分,第一部分是测试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。第二部分就是根据具体的代码来证实我们学到的知识,是具体的眼见为实。
        1、调试源码
            1.1、Example_14_1_1
     1 using System;
     2 using System.Threading;
     3 
     4 namespace Example_14_1_1
     5 {
     6     internal class Program
     7     {
     8         static void Main(string[] args)
     9         {
    10             var thread = new Thread(() =>
    11             {
    12                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId}");
    13                 Console.ReadLine();
    14             });
    15 
    16             thread.Start();
    17 
    18             Console.ReadLine();
    19         }
    20     }
    21 }
    View Code

            1.2、Example_14_1_2
     1 using System;
     2 using System.Diagnostics;
     3 using System.Threading;
     4 
     5 namespace Example_14_1_2
     6 {
     7     internal class Program
     8     {
     9         public static ManualResetEvent mre = new ManualResetEvent(false);
    10 
    11         static void Main(string[] args)
    12         {
    13             Console.WriteLine($"mre 默认为 false,即等待状态,请查看!");
    14             Debugger.Break();
    15 
    16             mre.Set();
    17             Console.WriteLine($"mre 默认为 true,即放行状态,请查看!");
    18             Debugger.Break();
    19 
    20             mre.Reset();
    21             Console.WriteLine($"mre Reset后为 false,即等待状态,请查看!");
    22             Debugger.Break();
    23         }
    24     }
    25 }
    View Code

            1.3、Example_14_1_3
     1 using System;
     2 using System.Diagnostics;
     3 using System.Threading;
     4 
     5 namespace Example_14_1_3
     6 {
     7     internal class Program
     8     {
     9         public static Semaphore sem = new Semaphore(1, 10);
    10         static void Main(string[] args)
    11         {
    12             for (int i = 0; i < int.MaxValue; i++)
    13             {
    14                 sem.Release();
    15                 Console.WriteLine("查看当前的 sem 值。");
    16                 Debugger.Break();
    17             }
    18         }
    19     }
    20 }
    View Code

            1.4、Example_14_1_4(Net 7.0)
     1 using System.Diagnostics;
     2 
     3 namespace Example_14_1_4_Core
     4 {
     5     internal class Program
     6     {
     7         public static Person person = new Person();
     8 
     9         static void Main(string[] args)
    10         {
    11             Task.Run(() =>
    12             {
    13                 lock (person)
    14                 {
    15                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中111111");
    16                     Debugger.Break();
    17                 }
    18             });
    19             Task.Run(() =>
    20             {
    21                 lock (person)
    22                 {
    23                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中222222");
    24                     Debugger.Break();
    25                 }
    26             });
    27             Console.ReadLine();
    28         }
    29     }
    30 
    31     public class Person
    32     {
    33     }
    34 }
    View Code

            1.5、Example_14_1_5
     1 using System;
     2 using System.Diagnostics;
     3 using System.Threading.Tasks;
     4 
     5 namespace Example_14_1_5
     6 {
     7     internal class Program
     8     {
     9         public static Person person = new Person();
    10 
    11         static void Main(string[] args)
    12         {
    13             Task.Run(() =>
    14             {
    15                 lock (person)
    16                 {
    17                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中");
    18                     Debugger.Break();
    19                 }
    20             });
    21             Console.ReadLine();
    22         }
    23     }
    24 
    25     public class Person
    26     {
    27     }
    28 }
    View Code

        2、眼见为实
            
            2.1、我们查看 C# Thread 线程所对应的 OS 层的数据结构表示。
                调试源码:Example_14_1_1
                这个项目调试的方法是不一样的,在这里,我们直接打开Debug 目录下的 EXE 应用程序,直接双击运行程序,程序启动成功,在控制台中输出:tid=3,这个值大家可能不一样。程序运行成功,就产生了一个线程对象。我们想要查看内核态线程的id,需要在借助一个【ProcessExplorer】工具,这个工具有32位和64位两个版本,根据自己系统特特性选择合适的版本,我选择的是64位版本的。效果如图:
                

                程序运行起来如下:
                

                接着,我们在过【通过名称过滤(Filter by name)】中输入我们项目的名称:Example_14_1_1,来进程查找。效果如图:
                
                我们在找到的进程上双击破,打开新窗口,如图:
                
                我们找到了我们项目进程的主键线程编号,然后就可以使用 Windbg 查看内核态的线程表示了。我们主线程的编号是:1204,这个是十进制的,要注意。
                然后,我们打开 Windbg,点击【File】-->【Attach to kernel(附加内核态)】,在右侧选择【local】,就是本机的内核态,点击【ok】按钮,进入调试界面。然后,我们使用【process】命令查找一下我们的项目。

    复制代码
     1 lkd> !process 0 2 Example_14_1_1.exe
     2 PROCESS ffff9004b47eb080
     3     SessionId: 1  Cid: 3a0c    Peb: 00322000  ParentCid: 24bc
     4     DirBase: 36353c002  ObjectTable: ffffc6096ce7b180  HandleCount: 194.
     5     Image: Example_14_1_1.exe
     6 
     7         THREAD ffff9004b64d2080  Cid 3a0c.04b4  Teb: 0000000000324000 Win32Thread: ffff9004b7232db0 WAIT: (Executive) KernelMode Alertable
     8             ffff9004b7310e68  NotificationEvent
     9 
    10         THREAD ffff9004b42e70c0  Cid 3a0c.0fb8  Teb: 000000000032d000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
    11             ffff9004b7352ae0  SynchronizationEvent
    12             ffff9004b7352760  SynchronizationEvent
    13             ffff9004b73524e0  SynchronizationEvent
    14 
    15         THREAD ffff9004b6b4f100  Cid 3a0c.3ab8  Teb: 0000000000330000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
    16             ffff9004a72b2b20  NotificationEvent
    17             ffff9004b7352660  SynchronizationEvent
    18             ffff9004b4d35a90  SynchronizationEvent
    19 
    20         THREAD ffff9004b63ea080  Cid 3a0c.318c  Teb: 0000000000333000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
    21             ffff9004b7353560  SynchronizationEvent
    复制代码

                他会把这个进程中的所有线程找出来。然后,我们点击【break】按钮,我们通过【ProcessExploler】看到我们项目的主线程是:1204,这个值是十进制的,我们看看十六进制是多少。

    1 lkd> ?0n1204
    2 Evaluate expression: 1204 = 00000000`000004b4

                然后,我们使用04b4查找一下,效果如图:
                

                ffff9004b64d2080 这个值就是线程的内核态的数据结构,我们可以继续使用【dt】命令查看一下详情。

      1 lkd> dt nt!_KThread ffff9004b64d2080
      2    +0x000 Header           : _DISPATCHER_HEADER
      3    +0x018 SListFaultAddress : (null) 
      4    +0x020 QuantumTarget    : 0x9c1aedd
      5    +0x028 InitialStack     : 0xfffff48b`1c777c50 Void
      6    +0x030 StackLimit       : 0xfffff48b`1c771000 Void
      7    +0x038 StackBase        : 0xfffff48b`1c778000 Void
      8    +0x040 ThreadLock       : 0
      9    +0x048 CycleTime        : 0x766fe16
     10    +0x050 CurrentRunTime   : 0
     11    +0x054 ExpectedRunTime  : 0x589722
     12    +0x058 KernelStack      : 0xfffff48b`1c777570 Void
     13    +0x060 StateSaveArea    : 0xfffff48b`1c777c80 _XSAVE_FORMAT
     14    +0x068 SchedulingGroup  : (null) 
     15    +0x070 WaitRegister     : _KWAIT_STATUS_REGISTER
     16    +0x071 Running          : 0 ''
     17    +0x072 Alerted          : [2]  ""
     18    +0x074 AutoBoostActive  : 0y1
     19    +0x074 ReadyTransition  : 0y0
     20    +0x074 WaitNext         : 0y0
     21    +0x074 SystemAffinityActive : 0y0
     22    +0x074 Alertable        : 0y1
     23    +0x074 UserStackWalkActive : 0y0
     24    +0x074 ApcInterruptRequest : 0y0
     25    +0x074 QuantumEndMigrate : 0y0
     26    +0x074 UmsDirectedSwitchEnable : 0y0
     27    +0x074 TimerActive      : 0y0
     28    +0x074 SystemThread     : 0y0
     29    +0x074 ProcessDetachActive : 0y0
     30    +0x074 CalloutActive    : 0y0
     31    +0x074 ScbReadyQueue    : 0y0
     32    +0x074 ApcQueueable     : 0y1
     33    +0x074 ReservedStackInUse : 0y0
     34    +0x074 UmsPerformingSyscall : 0y0
     35    +0x074 TimerSuspended   : 0y0
     36    +0x074 SuspendedWaitMode : 0y0
     37    +0x074 SuspendSchedulerApcWait : 0y0
     38    +0x074 CetUserShadowStack : 0y0
     39    +0x074 BypassProcessFreeze : 0y0
     40    +0x074 Reserved         : 0y0000000000 (0)
     41    +0x074 MiscFlags        : 0n16401
     42    +0x078 ThreadFlagsSpare : 0y00
     43    +0x078 AutoAlignment    : 0y1
     44    +0x078 DisableBoost     : 0y0
     45    +0x078 AlertedByThreadId : 0y0
     46    +0x078 QuantumDonation  : 0y1
     47    +0x078 EnableStackSwap  : 0y1
     48    +0x078 GuiThread        : 0y1
     49    +0x078 DisableQuantum   : 0y0
     50    +0x078 ChargeOnlySchedulingGroup : 0y0
     51    +0x078 DeferPreemption  : 0y0
     52    +0x078 QueueDeferPreemption : 0y0
     53    +0x078 ForceDeferSchedule : 0y0
     54    +0x078 SharedReadyQueueAffinity : 0y1
     55    +0x078 FreezeCount      : 0y0
     56    +0x078 TerminationApcRequest : 0y0
     57    +0x078 AutoBoostEntriesExhausted : 0y1
     58    +0x078 KernelStackResident : 0y1
     59    +0x078 TerminateRequestReason : 0y00
     60    +0x078 ProcessStackCountDecremented : 0y0
     61    +0x078 RestrictedGuiThread : 0y0
     62    +0x078 VpBackingThread  : 0y0
     63    +0x078 ThreadFlagsSpare2 : 0y0
     64    +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
     65    +0x078 ThreadFlags      : 0n205028
     66    +0x07c Tag              : 0 ''
     67    +0x07d SystemHeteroCpuPolicy : 0 ''
     68    +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
     69    +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
     70    +0x07f RunningNonRetpolineCode : 0y0
     71    +0x07f SpecCtrlSpare    : 0y0000000 (0)
     72    +0x07f SpecCtrl         : 0 ''
     73    +0x080 SystemCallNumber : 0x1a0006
     74    +0x084 ReadyTime        : 1
     75    +0x088 FirstArgument    : 0x00000000`00000094 Void
     76    +0x090 TrapFrame        : 0xfffff48b`1c777ac0 _KTRAP_FRAME
     77    +0x098 ApcState         : _KAPC_STATE
     78    +0x098 ApcStateFill     : [43]  "???"
     79    +0x0c3 Priority         : 9 ''
     80    +0x0c4 UserIdealProcessor : 2
     81    +0x0c8 WaitStatus       : 0n256
     82    +0x0d0 WaitBlockList    : 0xffff9004`b64d21c0 _KWAIT_BLOCK
     83    +0x0d8 WaitListEntry    : _LIST_ENTRY [ 0x00000000`00000000 - 0xffff9004`b1e95158 ]
     84    +0x0d8 SwapListEntry    : _SINGLE_LIST_ENTRY
     85    +0x0e8 Queue            : (null) 
     86    +0x0f0 Teb              : 0x00000000`00324000 Void
     87    +0x0f8 RelativeTimerBias : 0
     88    +0x100 Timer            : _KTIMER
     89    +0x140 WaitBlock        : [4] _KWAIT_BLOCK
     90    +0x140 WaitBlockFill4   : [20]  "p???"
     91    +0x154 ContextSwitches  : 0xee
     92    +0x140 WaitBlockFill5   : [68]  "p???"
     93    +0x184 State            : 0x5 ''
     94    +0x185 Spare13          : 0 ''
     95    +0x186 WaitIrql         : 0 ''
     96    +0x187 WaitMode         : 0 ''
     97    +0x140 WaitBlockFill6   : [116]  "p???"
     98    +0x1b4 WaitTime         : 0x780fc
     99    +0x140 WaitBlockFill7   : [164]  "p???"
    100    +0x1e4 KernelApcDisable : 0n-1
    101    +0x1e6 SpecialApcDisable : 0n0
    102    +0x1e4 CombinedApcDisable : 0xffff
    103    +0x140 WaitBlockFill8   : [40]  "p???"
    104    +0x168 ThreadCounters   : (null) 
    105    +0x140 WaitBlockFill9   : [88]  "p???"
    106    +0x198 XStateSave       : (null) 
    107    +0x140 WaitBlockFill10  : [136]  "p???"
    108    +0x1c8 Win32Thread      : 0xffff9004`b7232db0 Void
    109    +0x140 WaitBlockFill11  : [176]  "p???"
    110    +0x1f0 Ucb              : (null) 
    111    +0x1f8 Uch              : (null) 
    112    +0x200 ThreadFlags2     : 0n0
    113    +0x200 BamQosLevel      : 0y00000000 (0)
    114    +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
    115    +0x204 Spare21          : 0
    116    +0x208 QueueListEntry   : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
    117    +0x218 NextProcessor    : 0
    118    +0x218 NextProcessorNumber : 0y0000000000000000000000000000000 (0)
    119    +0x218 SharedReadyQueue : 0y0
    120    +0x21c QueuePriority    : 0n0
    121    +0x220 Process          : 0xffff9004`b47eb080 _KPROCESS
    122    +0x228 UserAffinity     : _GROUP_AFFINITY
    123    +0x228 UserAffinityFill : [10]  "???"
    124    +0x232 PreviousMode     : 1 ''
    125    +0x233 BasePriority     : 8 ''
    126    +0x234 PriorityDecrement : 0 ''
    127    +0x234 ForegroundBoost  : 0y0000
    128    +0x234 UnusualBoost     : 0y0000
    129    +0x235 Preempted        : 0 ''
    130    +0x236 AdjustReason     : 0 ''
    131    +0x237 AdjustIncrement  : 0 ''
    132    +0x238 AffinityVersion  : 0x28
    133    +0x240 Affinity         : _GROUP_AFFINITY
    134    +0x240 AffinityFill     : [10]  "???"
    135    +0x24a ApcStateIndex    : 0 ''
    136    +0x24b WaitBlockCount   : 0x1 ''
    137    +0x24c IdealProcessor   : 2
    138    +0x250 NpxState         : 5
    139    +0x258 SavedApcState    : _KAPC_STATE
    140    +0x258 SavedApcStateFill : [43]  "???"
    141    +0x283 WaitReason       : 0 ''
    142    +0x284 SuspendCount     : 0 ''
    143    +0x285 Saturation       : 0 ''
    144    +0x286 SListFaultCount  : 0
    145    +0x288 SchedulerApc     : _KAPC
    146    +0x288 SchedulerApcFill0 : [1]  "??????"
    147    +0x289 ResourceIndex    : 0x1 ''
    148    +0x288 SchedulerApcFill1 : [3]  "???"
    149    +0x28b QuantumReset     : 0x6 ''
    150    +0x288 SchedulerApcFill2 : [4]  "???"
    151    +0x28c KernelTime       : 3
    152    +0x288 SchedulerApcFill3 : [64]  "???"
    153    +0x2c8 WaitPrcb         : (null) 
    154    +0x288 SchedulerApcFill4 : [72]  "???"
    155    +0x2d0 LegoData         : (null) 
    156    +0x288 SchedulerApcFill5 : [83]  "???"
    157    +0x2db CallbackNestingLevel : 0 ''
    158    +0x2dc UserTime         : 0
    159    +0x2e0 SuspendEvent     : _KEVENT
    160    +0x2f8 ThreadListEntry  : _LIST_ENTRY [ 0xffff9004`b42e73b8 - 0xffff9004`b47eb0b0 ]
    161    +0x308 MutantListHead   : _LIST_ENTRY [ 0xffff9004`b64d2388 - 0xffff9004`b64d2388 ]
    162    +0x318 AbEntrySummary   : 0x3e '>'
    163    +0x319 AbWaitEntryCount : 0 ''
    164    +0x31a AbAllocationRegionCount : 0 ''
    165    +0x31b SystemPriority   : 0 ''
    166    +0x31c SecureThreadCookie : 0
    167    +0x320 LockEntries      : 0xffff9004`b64d26d0 _KLOCK_ENTRY
    168    +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
    169    +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
    170    +0x338 PriorityFloorCounts : [16]  ""
    171    +0x348 PriorityFloorCountsReserved : [16]  ""
    172    +0x358 PriorityFloorSummary : 0
    173    +0x35c AbCompletedIoBoostCount : 0n0
    174    +0x360 AbCompletedIoQoSBoostCount : 0n0
    175    +0x364 KeReferenceCount : 0n0
    176    +0x366 AbOrphanedEntrySummary : 0 ''
    177    +0x367 AbOwnedEntryCount : 0x1 ''
    178    +0x368 ForegroundLossTime : 0
    179    +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
    180    +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
    181    +0x378 InGlobalForegroundList : 0
    182    +0x380 ReadOperationCount : 0n12
    183    +0x388 WriteOperationCount : 0n0
    184    +0x390 OtherOperationCount : 0n293
    185    +0x398 ReadTransferCount : 0n27743
    186    +0x3a0 WriteTransferCount : 0n0
    187    +0x3a8 OtherTransferCount : 0n9406
    188    +0x3b0 QueuedScb        : (null) 
    189    +0x3b8 ThreadTimerDelay : 0
    190    +0x3bc ThreadFlags3     : 0n0
    191    +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
    192    +0x3bc PpmPolicy        : 0y00
    193    +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
    194    +0x3c0 TracingPrivate   : [1] 0
    195    +0x3c8 SchedulerAssist  : (null) 
    196    +0x3d0 AbWaitObject     : (null) 
    197    +0x3d8 ReservedPreviousReadyTimeValue : 0
    198    +0x3e0 KernelWaitTime   : 0xe
    199    +0x3e8 UserWaitTime     : 0
    200    +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
    201    +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
    202    +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
    203    +0x400 SchedulerAssistPriorityFloor : 0n0
    204    +0x404 Spare28          : 0
    205    +0x408 EndPadding       : [5] 0
    View Code

                大家感兴趣的,可以打开看看,内容还是不少的。
                当然,我们也可以通过 Windbg 直接查看了,我们的项目正在执行中,所以我们可以通过【Attach to process】进入调试界面,然后,通过【!t】或者【!threads】命令,查看线程三者的对应关系。

    复制代码
     1 0:004> !t
     2 ThreadCount:      3
     3 UnstartedThread:  0
     4 BackgroundThread: 1
     5 PendingThread:    0
     6 DeadThread:       0
     7 Hosted Runtime:   no
     8                                                                          Lock  
     9        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
    10    0    1  4b4 00696b10     2a020 Preemptive  02506254:00000000 006903d0 1     MTA 
    11    2    2 3ab8 00698df8     2b220 Preemptive  00000000:00000000 006903d0 0     MTA (Finalizer) 
    12    3    3 318c 006ee308   202b020 Preemptive  0250501C:00000000 006903d0 0     MTA 
    13 0:004> !threads
    14 ThreadCount:      3
    15 UnstartedThread:  0
    16 BackgroundThread: 1
    17 PendingThread:    0
    18 DeadThread:       0
    19 Hosted Runtime:   no
    20                                                                          Lock  
    21        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
    22    0    1  4b4 00696b10     2a020 Preemptive  02506254:00000000 006903d0 1     MTA 
    23    2    2 3ab8 00698df8     2b220 Preemptive  00000000:00000000 006903d0 0     MTA (Finalizer) 
    24    3    3 318c 006ee308   202b020 Preemptive  0250501C:00000000 006903d0 0     MTA 
    复制代码

                ID是1就是C#的托管线程编号, OSID的值是4b4就是操作系统层面的线程的数据结构,ThreadOBJ 就是CLR 层面的线程。    


            2.2、我们看看 AutoResetEvent 是如何通过 bool 值变化实现线程间的同步的。
                调试源码:Example_14_1_2
                我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们需要使用【g】命令,继续运行程序,在【Debugger.Break()】语句处停止,我们的控制台应用程序输出:mre 默认为 false,即等待状态,请查看!Windbg 处于暂停状态,我们就可以调试了。
                首先,我们去托管堆中查找一下 ManualResetEvent 这个对象,执行【!dumpheap -type ManualResetEvent】命令
    复制代码
    1 1:000> !dumpheap -type ManualResetEvent
    2  Address       MT     Size
    3 033e24d4 6d53d578       24     
    4 
    5 Statistics:
    6       MT    Count    TotalSize Class Name
    7 6d53d578        1           24 System.Threading.ManualResetEvent
    8 Total 1 objects
    复制代码

                红色标注的地址就是我们要找的 ManualResetEvent 的实例。我们继续使用【!do】命令查看详情。

    复制代码
     1 1:000> !do 033e24d4
     2 Name:        System.Threading.ManualResetEvent
     3 MethodTable: 6d53d578
     4 EEClass:     6d6114d0
     5 Size:        24(0x18) bytes
     6 File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
     7 Fields:
     8       MT    Field   Offset                 Type VT     Attr    Value Name
     9 6d4f2734  40005ba        4        System.Object  0 instance 00000000 __identity
    10 6d4f7b18  4001990        c        System.IntPtr  1 instance      2f8 waitHandle
    11 6d4f6688  4001991        8 ...es.SafeWaitHandle  0 instance 033e2504 safeWaitHandle
    12 6d4f878c  4001992       10       System.Boolean  1 instance        0 hasThreadAffinity
    13 6d4f7b18  4001993      ec8        System.IntPtr  1   shared   static InvalidHandle
    14     >> Domain:Value  016adb18:ffffffff <<
    复制代码

                红色标注的是一个 handle 对象,我们可以使用【!handle 2f8 f】命令继续查看,必须具有 f 参数。

    复制代码
     1 1:000> !handle 2f8 f
     2 Handle 2f8
     3   Type             Event
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     32769
    10   Name             
    11   Object Specific Information
    12     Event Type Manual Reset(事件类型是 ManualResetEvent)
    13     Event is Waiting(当前是等待状态)
    复制代码

                说明 false 是等待的状态,然后,我们继续【g】运行一下,等我们的控制台项目输出:mre 默认为 true,即放行状态,请查看!我们继续执行【!handle 2f8 f】命令查看。

    复制代码
     1 1:000> !handle 2f8 f
     2 Handle 2f8
     3   Type             Event
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     65536
    10   Name             
    11   Object Specific Information
    12     Event Type Manual Reset
    13     Event is Set
    复制代码

                然后,我们继续【g】运行一下,等我们的控制台项目输出:mre Reset后为 false,即等待状态,请查看!我们继续执行【!handle 2f8 f】命令查看。

    复制代码
     1 1:000> !handle 2f8 f
     2 Handle 2f8
     3   Type             Event
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     65535
    10   Name             
    11   Object Specific Information
    12     Event Type Manual Reset
    13     Event is Waiting
    复制代码

     

                我们都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底层的功能,说白了就是 C# 只是使用了 Windows 内核提供的事件,C# 不过是对其进行了包装,如果你想要查看内存地址,必须到内核态去看。

            2.3、如何到内核态去查看 AutoResetEvent 和 ManulResetEvent 地址。
                调试源码:Example_14_1_2
                在这里,我们要打开两个 Windbg,第一个 Windbg 我们查看一下用户态。我们编译程序,通过【File】-->【launche executing】附加我们的可执行程序。进入到调试器界面,我们继续【g】,我们的控制台应用程序输出:mre 默认为 false,即等待状态,请查看!调试器处于中断状态,我们就可以调试了。
                我们首先要找到【ManualResetEvent】对象的事件句柄,执行命令【!dumpheap -type ManualResetEvent】命令。
    复制代码
    0:000> !dumpheap -type ManualResetEvent
     Address       MT     Size
    033224d4 6d53d578       24     
    
    Statistics:
          MT    Count    TotalSize Class Name
    6d53d578        1           24 System.Threading.ManualResetEvent
    Total 1 objects
    复制代码

                红色标注的就是【ManualResetEvent】对象地址,我们可以使用【!dumpobj /d 033224d4】命令查看 ManualResetEvent 实例对象。

    复制代码
     1 0:000> !dumpobj /d 033224d4
     2 Name:        System.Threading.ManualResetEvent
     3 MethodTable: 6d53d578
     4 EEClass:     6d6114d0
     5 Size:        24(0x18) bytes
     6 File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
     7 Fields:
     8       MT    Field   Offset                 Type VT     Attr    Value Name
     9 6d4f2734  40005ba        4        System.Object  0 instance 00000000 __identity
    10 6d4f7b18  4001990        c        System.IntPtr  1 instance      2dc waitHandle(我们要查找的事件句柄)
    11 6d4f6688  4001991        8 ...es.SafeWaitHandle  0 instance 03322504 safeWaitHandle
    12 6d4f878c  4001992       10       System.Boolean  1 instance        0 hasThreadAffinity
    13 6d4f7b18  4001993      ec8        System.IntPtr  1   shared   static InvalidHandle
    14     >> Domain:Value  01532430:ffffffff <<
    复制代码

                我们再打开一个 Windbg,查看内核态,点击【File】-->【Attach to Kernel】,右侧选择【local】,点击【ok】进入调试器界面。2dc是一个句柄,就像一个编号,我们还需要借助【Process Explorer】工具,我们打开这个工具,然后在【Filter by name】输入项目名称Example_14_1_2,结果如图:
                

                我们在 0X000002dc行双击,打开新窗口,效果如图:
                

                我们就找到了内核地址了。然后,我们到 Windbg 的内核态中去查看一下这个地址,使用【dp】命令。当前值:0(00000000

    1 lkd> dp 0xFFFF9004B7B916E0 l1
    2 ffff9004`b7b916e0  00000000`00060000

                然后我们【g】一下用户态的 Windbg,控制台输出:mre 默认为 true,即放行状态,请查看!当前值:1(00000001),然后切换到【内核态】的Windbg,继续使用【dp】命令,查看一下。

    1 lkd> dp 0xFFFF9004B7B916E0 l1
    2 ffff9004`b7b916e0  00000001`00060000

                然后,我们再【g】一下【用户态】的Windbg,控制台输出:mre Reset后为 false,即等待状态,请查看!当前值:0(00000000),然后切换到【内核态】的Windbg,继续使用【dp】命令,查看一下。

    1 lkd> dp 0xFFFF9004B7B916E0 l1
    2 ffff9004`b7b916e0  00000000`00060000

                我们就看到了,状态是0和1相互切换的。


            2.4、我们查看 Semaphore Count 的值是如何变化的。
                调试源码:Example_14_1_3
                我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们需要使用【g】命令,继续运行程序,在【Debugger.Break()】语句处停止,我们的控制台应用程序输出:查看当前的 sem 值。现在就可以调试程序了。
                我们现在托管堆中查找一下 Semaphore 对象,我们可以使用【!dumpheap -type Semaphore】命令。
    复制代码
    1 0:000> !dumpheap -type Semaphore
    2  Address       MT     Size
    3 02f924d4 6d59611c       24     
    4 
    5 Statistics:
    6       MT    Count    TotalSize Class Name
    7 6d59611c        1           24 System.Threading.Semaphore
    8 Total 1 objects
    复制代码

                红色标注的地址 02f924d4 就是 Semaphore 对象,然后,我们可以使用【!do 02f924d4】或者【!dumpobj /d 02f924d4】查看 Semaphore 对象的详情,两个命令执行的结果都是一样的。

    复制代码
     1 0:000> !do 02f924d4
     2 Name:        System.Threading.Semaphore
     3 MethodTable: 6d59611c
     4 EEClass:     6d5ccfa0
     5 Size:        24(0x18) bytes
     6 File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
     7 Fields:
     8       MT    Field   Offset                 Type VT     Attr    Value Name
     9 6ec52734  40005ba        4        System.Object  0 instance 00000000 __identity
    10 6ec57b18  4001990        c        System.IntPtr  1 instance      314 waitHandle
    11 6ec56688  4001991        8 ...es.SafeWaitHandle  0 instance 02f92504 safeWaitHandle
    12 6ec5878c  4001992       10       System.Boolean  1 instance        0 hasThreadAffinity
    13 6ec57b18  4001993      ec8        System.IntPtr  1   shared   static InvalidHandle
    14     >> Domain:Value  010dd880:ffffffff <<
    15 
    16 
    17 0:000> !dumpobj /d 02f924d4
    18 Name:        System.Threading.Semaphore
    19 MethodTable: 6d59611c
    20 EEClass:     6d5ccfa0
    21 Size:        24(0x18) bytes
    22 File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
    23 Fields:
    24       MT    Field   Offset                 Type VT     Attr    Value Name
    25 6ec52734  40005ba        4        System.Object  0 instance 00000000 __identity
    26 6ec57b18  4001990        c        System.IntPtr  1 instance      314 waitHandle
    27 6ec56688  4001991        8 ...es.SafeWaitHandle  0 instance 02f92504 safeWaitHandle
    28 6ec5878c  4001992       10       System.Boolean  1 instance        0 hasThreadAffinity
    29 6ec57b18  4001993      ec8        System.IntPtr  1   shared   static InvalidHandle
    30     >> Domain:Value  010dd880:ffffffff <<
    复制代码

                Semaphore 其实也是一个 waitHandle,我们有了 handle 地址,就可以使用【!handle】命令了。

    复制代码
     1 Handle 314
     2   Type             Semaphore
     3   Attributes       0
     4   GrantedAccess    0x1f0003:
     5          Delete,ReadControl,WriteDac,WriteOwner,Synch
     6          QueryState,ModifyState
     7   HandleCount      2
     8   PointerCount     65536
     9   Name             
    10   Object Specific Information
    11     Semaphore Count 2(Semaphore sem = new Semaphore(1, 10),我们初始值是1,当前值是2)
    12     Semaphore Limit 10
    复制代码

                我们继续【g】,然后再次执行【!handle 314 f】命令,再次查看,Semaphore Count 的值就是3。

    复制代码
     1 0:000> !handle 314 f
     2 Handle 314
     3   Type             Semaphore
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     65535
    10   Name             
    11   Object Specific Information
    12     Semaphore Count 3(现在值是:3,每运行一次,该值就增加1,调用 Release()函数一次,值就增加一次。)
    13     Semaphore Limit 10(这个值是极限值)
    复制代码

                我们继续【g】,然后再次执行【!handle 314 f】命令,再次查看,Semaphore Count 值肯定就是4了。

    复制代码
     1 0:000> !handle 314 f
     2 Handle 314
     3   Type             Semaphore
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     65534
    10   Name             
    11   Object Specific Information
    12     Semaphore Count 4(又增加了一次)
    13     Semaphore Limit 10
    复制代码

     

                这个变化的 Count 值的内存地址在哪里呢?其实它的功能都是有内核态提供的,如果想看 Count 的内存地址,必须找到内核态的地址。这里我们还是需要借用【Process Explorer】工具,我使用的64位版本,自己可以根据自己系统的特点选择。打开工具,过滤我们的项目【Example_14_1_3】。效果如图:

                
                双击【Semaphore】条目打卡属性窗口,就能看到它的内核态的内存地址了。
                

                我们有了内核态的内存地址,就需要再打开一个 Windbg,点击【File】-->【Attach to Kernel】,在右侧窗口选择【local】点击【ok】打开调试器。然后,我们就可以使用【dp】命令查看具体的值了,当前值是:4(00000004)。

    1 lkd> dp 0xFFFFCE09E477EA60 l1 
    2 ffffce09`e477ea60  00000004`00080005

                我们切换到第一个 Windbg 窗口,【g】继续运行,然后再切换回来这个 Windbg,再次运行【dp】命令,当前的值应该就是:5(00000005)。

    1 lkd> dp 0xFFFFCE09E477EA60 l1 
    2 ffffce09`e477ea60  00000005`00080005

                其实,我们可以在第一个 Windbg 窗口中,使用【!handle 314 f】也可以看到结果值,肯定也是5。

    复制代码
     1 0:000> !handle 314 f
     2 Handle 314
     3   Type             Semaphore
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     65532
    10   Name             
    11   Object Specific Information
    12     Semaphore Count 5
    13     Semaphore Limit 10
    复制代码

                    Semaphore 是有极限值的,如果超过极限值,CLR 会抛出异常。

    复制代码
     1 0:000> g
     2 ModLoad: 058f0000 059ea000   image058f0000
     3 ModLoad: 059f0000 05aea000   image059f0000
     4 (52c.2990): CLR exception - code e0434352 (first chance)
     5 ModLoad: 66bf0000 66cf5000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\diasymreader.dll
     6 ModLoad: 6cd30000 6d548000   C:\Windows\assembly\NativeImages_v4.0.30319_32\System.Core\System.Core.ni.dll
     7 ModLoad: 73630000 73643000   C:\Windows\SysWOW64\CRYPTSP.dll
     8 ModLoad: 73600000 7362f000   C:\Windows\SysWOW64\rsaenh.dll
     9 ModLoad: 75ce0000 75cf9000   C:\Windows\SysWOW64\bcrypt.dll
    10 ModLoad: 73690000 7369a000   C:\Windows\SysWOW64\CRYPTBASE.dll
    11 (52c.2990): CLR exception - code e0434352 (!!! second chance !!!)
    12 *** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\\System.ni.dll
    13 eax=00f3ec58 ebx=00000005 ecx=00000005 edx=00000000 esi=00f3ed1c edi=00000001
    14 eip=75969862 esp=00f3ec58 ebp=00f3ecb4 iopl=0         nv up ei pl nz ac po nc
    15 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
    16 KERNELBASE!RaiseException+0x62:
    17 75969862 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:00f3ecac=c4689540
    复制代码

                

            2.5、我们使用 Windbg 查看 Monitor 的实现,该项目是 Net 7.0,因为Net Framework 是闭源的,没有办法看到源码。
                调试源码:Example_14_1_4
                我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们需要使用【g】命令,继续运行程序,当我们的控制台程序输出:4 已进入 Person 锁中111111(这里不一定是这个,我的输出是这个),这个过程的时间有点长,Windbg执行框处在【busy】状态, 因为正在下载 coreclr.pdb,下载完毕就可以,操作完成,Windbg 有一个 int 3 中断,就可以调试程序了。
                然后,我们使用【!syncblk】命令,查看一下同步块。

    复制代码
    1 0:007> !syncblk
    2 Index         SyncBlock   MonitorHeld   Recursion   Owning Thread Info          SyncBlock Owner
    3     9 0000026C988CE368              3           1   000002AD2EE66800 18b0   7   0000026c9cc0cb88 Example_14_1_4_Core.Person
    4 -----------------------------
    5 Total           12
    6 CCW             0
    7 RCW             0
    8 ComClassFactory 0
    9 Free            0
    复制代码


                我们说过 Monitor 的底层实现就是 AwareLock,这个标红 0000026C988CE368 地址就是指向  AwareLock。我们使用【dt】命令查看一番。

    复制代码
    1 0:007> dt coreclr!AwareLock 0000026C988CE368
    2    +0x000 m_lockState      : AwareLock::LockState(底层的 awarelock)
    3    +0x004 m_Recursion      : 1(递归次数1)
    4    +0x008 m_HoldingThread  : 0x000002ad`2ee66800 Thread(持有的线程,和 Owning Thread Info 值一样)
    5    +0x010 m_TransientPrecious : 0n1
    6    +0x014 m_dwSyncIndex    : 0x80000009(这个就是同步块索引,是9)
    7    +0x018 m_SemEvent       : CLREvent(底层还是使用的 Event 实现同步)
    8    +0x028 m_waiterStarvationStartTimeMs : 0xb6cb0b
    复制代码

                我们继续使用【dx】命令查看 m_SemEvent 是什么。

    1 0:007> dx -r1 (*((coreclr!CLREvent *)0x26c988ce380))
    2 (*((coreclr!CLREvent *)0x26c988ce380))                 [Type: CLREvent]
    3     [+0x000] m_handle         : 0x2d0 [Type: void *](这里是一个句柄)
    4     [+0x008] m_dwFlags        : 0xd [Type: Volatilelong>]

                既然是一个 handle,我们就使用【!handle】命令查看一下就知道了。

    复制代码
     1 0:007> !handle 0x2d0 f
     2 Handle 2d0
     3   Type             Event
     4   Attributes       0
     5   GrantedAccess    0x1f0003:
     6          Delete,ReadControl,WriteDac,WriteOwner,Synch
     7          QueryState,ModifyState
     8   HandleCount      2
     9   PointerCount     65537
    10   Name             
    11   Object Specific Information
    12     Event Type Auto Reset(其实就是 AutoResetEvent)
    13     Event is Waiting
    复制代码


            2.6、我们看看 ThinLock 锁的实现逻辑。
                调试源码:Example_14_1_5
                我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们需要使用【g】命令,继续运行程序,我们的控制台会输出:3 已进入 Person 锁中。此时,我们的 Windbg 处于 int 3 中断的状态,就可以调试程序了。
                我们还是先使用【!syncblk】命令,查看一下同步块。

    复制代码
    1 0:009> !syncblk
    2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
    3 -----------------------------
    4 Total           6
    5 CCW             1
    6 RCW             2
    7 ComClassFactory 0
    8 Free            0
    复制代码

                没有同步块,这就说明虽然用到了锁,但是没有用到同步块。我们既然想要查看这个Person对象的对象,那我们就现在托管堆中找到这个对象,可以使用【!dumpheap -type Person】命令,完成这个操作。

    复制代码
    1 0:009> !dumpheap -type Person
    2  Address       MT     Size
    3 02c224d4 01224e0c       12     
    4 
    5 Statistics:
    6       MT    Count    TotalSize Class Name
    7 01224e0c        1           12 Example_14_1_5.Person
    8 Total 1 objects
    复制代码

                红色标注的地址就是 Person 对象的地址。我们可以使用【!dp】命令来查看。

    1 0:009> dp 02c224d4-0x4 l4
    2 02c224d0  00000003 01224e0c 00000000 00000000

                同步块索引的值是3(00000003),这个3 就是持有锁的线程 id 值。我们可以使用【!t】或者【!threads】命令查看一下当前的线程。

    复制代码
     1 0:009> !t
     2 ThreadCount:      4
     3 UnstartedThread:  0
     4 BackgroundThread: 3
     5 PendingThread:    0
     6 DeadThread:       0
     7 Hosted Runtime:   no
     8                                                                          Lock  
     9        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
    10    0    1 3b00 00cb9760     2a020 Preemptive  02C2A044:00000000 00c823e8 1     MTA 
    11    5    2 174c 00c893b8     2b220 Preemptive  00000000:00000000 00c823e8 0     MTA (Finalizer) 
    12    9    3 29fc 00cf0fe8   1029220 Preemptive  02C2742C:00000000 00c823e8 1     MTA (Threadpool Worker) (这个就是持有锁的线程,id=3)
    13   11    4 37a8 00cf4af8   1029220 Preemptive  02C281E8:00000000 00c823e8 0     MTA (Threadpool Worker) 
    复制代码

                我们知道了线程 id,我们就可以切换到该线程上去看看那调用栈是什么样子的。

    复制代码
    1 0:003> ~~[29fc]s
    2 eax=0567f124 ebx=00000000 ecx=00cf0fe8 edx=0567f55c esi=02c27244 edi=0567f168
    3 eip=7599f262 esp=0567f0bc ebp=0567f148 iopl=0         nv up ei pl zr na pe nc
    4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000244
    5 KERNELBASE!wil::details::DebugBreak+0x2:
    6 7599f262 cc              int     3
    复制代码

                然后,我们看看线程栈。

    复制代码
     1 0:009> !clrstack
     2 OS Thread Id: 0x29fc (9)
     3 Child SP       IP Call Site
     4 0567f0d4 7599f262 [HelperMethodFrame: 0567f0d4] System.Diagnostics.Debugger.BreakInternal()
     5 0567f150 6f7cf195 System.Diagnostics.Debugger.Break() [f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs @ 91]
     6 0567f178 012b0a90 Example_14_1_5.Program+c.b__1_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_14_1_5\Program.cs @ 18]
     7 0567f1c0 6f09d4bb System.Threading.Tasks.Task.InnerInvoke() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884]
     8 0567f1cc 6f09b731 System.Threading.Tasks.Task.Execute() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498]
     9 0567f1f0 6f09b6fc System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861]
    10 0567f1f4 6f038604 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980]
    11 0567f260 6f038537 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928]
    12 0567f274 6f09b4b2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827]
    13 0567f2d8 6f09b357 System.Threading.Tasks.Task.ExecuteEntry(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767]
    14 0567f2e8 6f09b29d System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704]
    15 0567f2ec 6f00eb7d System.Threading.ThreadPoolWorkQueue.Dispatch() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820]
    16 0567f33c 6f00e9db System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161]
    17 0567f55c 70def036 [DebuggerU2MCatchHandlerFrame: 0567f55c] 
    复制代码

                红色标注的就是我们程序暂停的位置,VisualStudio 所对应的代码行数。
                其实,我们获取到了对象地址,可以【!do】一下,也可以看到一些信息。

    复制代码
    1 0:009> !do 02c224d4
    2 Name:        Example_14_1_5.Person
    3 MethodTable: 01224e0c
    4 EEClass:     0122135c
    5 Size:        12(0xc) bytes
    6 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_14_1_5\bin\Debug\Example_14_1_5.exe
    7 Fields:
    8 None
    9 ThinLock owner 3 (00cf0fe8), Recursive 0
    复制代码



    四、总结
        终于写完了。还是老话,虽然很忙,写作过程也挺累的,但是看到了自己的成长,心里还是挺快乐的。学习过程真的没那么轻松,还好是自己比较喜欢这一行,否则真不知道自己能不能坚持下来。老话重谈,《高级调试》的这本书第一遍看,真的很晕,第二遍稍微好点,不学不知道,一学吓一跳,自己欠缺的很多。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。

  • 相关阅读:
    了解一下公网IP和域名的区别与联系
    c# 基础习题答案 20240709
    c++千万数据级别正确使用无锁队列,避免内存撕碎(二)
    动画云渲染要多少钱?云渲染怎么使用?
    String 创建字符串对象和字符串常量池的联系推理
    10 Go语言中接口运算,空接口
    7.2.7 【MySQL】用于分组
    【JUC】并发需要了解的关键字volatile
    Java HashSet
    RocketMQ消息生产者是如何选择Broker的
  • 原文地址:https://www.cnblogs.com/PatrickLiu/p/17910805.html