Net 高階偵錯之十四:執行緒同步的基礎知識和常見的同步原語

2023-12-19 15:00:15
一、介紹
    今天是《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             <none>
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             <none>
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             <none>
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             <none>
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             <none>
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             <none>
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             <none>
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: Volatile<unsigned long>]

            既然是一個 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             <none>
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



四、總結
    終於寫完了。還是老話,雖然很忙,寫作過程也挺累的,但是看到了自己的成長,心裡還是挺快樂的。學習過程真的沒那麼輕鬆,還好是自己比較喜歡這一行,否則真不知道自己能不能堅持下來。老話重談,《高階偵錯》的這本書第一遍看,真的很暈,第二遍稍微好點,不學不知道,一學嚇一跳,自己欠缺的很多。好了,不說了,不忘初心,繼續努力,希望老天不要辜負努力的人。