Net 高級調試之十五:經典的鎖故障

一、簡介
    今天是《Net 高級調試》的第十五篇文章,這個系列的文章也快結束了,但是我們深入學習的腳步還不能停止。上一篇文件我們介紹了C# 中一些鎖的實現邏輯,並做到了眼見爲實的演示給大家它們底層是如何實現的,今天這篇文件就主要介紹一些如何查找和解決在項目調試中遇到的鎖的問題,比如:死鎖、孤立鎖、線程中止和終結期掛起,我們會看到表象是什麼,也會做到遇到這樣問題,我們如何解決問題,我們每一個操作都能做到有的放矢。我們學了鎖的實現,現在又要學習有關鎖的解決辦法,就是讓我們做到知其一,也要知其二,這些是 Net 框架的底層,瞭解更深,對於我們調試更有利。當然了,第一次看視頻或者看書,是很迷糊的,不知道如何操作,還是那句老話,一遍不行,那就再來一遍,還不行,那就再來一遍,俗話說的好,書讀千遍,其意自現。
     如果在沒有說明的情況下,所有代碼的測試環境都是 Net Framewok 4.8,但是,有時候爲了查看源碼,可能需要使用 Net Core 的項目,我會在項目章節裏進行說明。好了,廢話不多說,開始我們今天的調試工作。

       調試環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
          操作系統:Windows Professional 10
          調試工具:Windbg Preview(可以去Microsoft Store 去下載)
          開發工具:Visual Studio 2022
          Net 版本:Net Framework 4.8
          CoreCLR源碼:源碼下載

二、基礎知識

    在 C# 編程中會經常使用到 lock 鎖,其實就是 Monitor 的語法糖,如果使用不好,經常會出現鎖問題,經典的有:死鎖、孤兒鎖、線程中止和異常。這篇文章主要針對:死鎖、孤兒鎖和線程中止做介紹。

    1、死鎖
        開中最長遇到的就是死鎖,在沒有【!dlk】(這個命令是 SOSEX.dll 功能,不是SOS.dll 功能,可能很多人會問,既然有這個命令,我們直接使用這個命令不就可以了嗎,其實不然,dlk 包含在 SOSEX.dll 中,但是 SOSEX.dll只適合在 Net Framework 框架中使用,如果在 Net 5.0、6.0、7.0或者更高的版本是使用不了的)命令的加持下想解決問題還是有點困難的,但是手工分析和調試也是一個非常重要的基本功,也是十分考究C# 基本功的能力。
            思路如下:
                a、觀察同步塊表
                b、切換到鎖線程,查看 clr!AwareLock-Enter+0x4a 在等待什麼對象。
                
    2、孤兒鎖(異常)        
         孤兒鎖是因爲開發者使用 Monitor.Enter 獲取一個對象後,因爲某種原因沒有正確調用 Monitor.Exit,導致這個對象一直處於佔用狀態,其他線程也就無法進入了,強烈建議使用 lock 語法。

    3、線程的銷燬
        線程銷燬導致的 lock 鎖未釋放,尋找起來難度也很大,這種場景經常出現在和(非託管代碼)交互的場景下,所以開發界限要明確,責任要清楚,代碼做到高內聚低耦合,纔會更安全。

三、源碼調試
    廢話不多說,這一節是具體的調試過程,又可以說是眼見爲實的過程,在開始之前,我還是要囉嗦兩句,這一節分爲兩個部分,第一部分是測試的源碼部分,沒有代碼,當然就談不上測試了,調試必須有載體。第二部分就是根據具體的代碼來證實我們學到的知識,是具體的眼見爲實。
    1、調試源碼
        1.1、Example_15_1_1
 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace Example_15_1_1
 6 {
 7     internal class Program
 8     {
 9         public static Person person = new Person();
10         public static Student student = new Student();
11         static void Main(string[] args)
12         {
13             Task.Run(() =>
14             {
15                 lock (person)
16                 {
17                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Person(1111) 鎖");
18                     Thread.Sleep(1000);
19                     lock (student)
20                     {
21                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Student(1111) 鎖");
22                         Console.ReadLine();
23                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Student(1111) 鎖");
24                     }
25                 }
26             });
27 
28             Task.Run(() =>
29             {
30                 lock (student)
31                 {
32                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Student(22222) 鎖");
33                     Thread.Sleep(1000);
34                     lock (person)
35                     {
36                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Person(22222) 鎖");
37                         Console.ReadLine();
38                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Person(22222) 鎖");
39                     }
40                 }
41             });
42 
43             Console.ReadLine();
44         }
45     }
46 
47     public class Student { }
48 
49     public class Person { }
50 }
View Code

        1.2、Example_15_1_2
 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace Example_15_1_2
 6 {
 7     internal class Program
 8     {
 9         public static Person person = new Person();
10         static void Main(string[] args)
11         {
12             Task.Run(() =>
13             {
14                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},準備進入 Person(1111) ");
15                 try
16                 {
17                     Monitor.Enter(person);
18                     Thread.Sleep(1000);
19 
20                     var returnValue = 10 / Convert.ToInt32("0");
21 
22                     Monitor.Exit(person);
23                 }
24                 catch (Exception ex)
25                 {
26                     Console.WriteLine(ex.Message);
27                 }
28                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Person(1111) ");
29             });
30 
31             Console.WriteLine("準備開啓第二線程,準備進入鎖");
32 
33             Task.Run(() =>
34             {
35                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},準備進入 Person(22222) ");
36 
37                 Monitor.Enter(person);
38                 Thread.Sleep(1000);
39                 Monitor.Exit(person);
40 
41                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Person(222222) ");
42             });
43 
44             Console.ReadLine();
45         }
46     }
47 
48     public class Person { }
49 }
View Code

        1.3、Example_15_1_3
 1 using System;
 2 using System.Runtime.InteropServices;
 3 using System.Threading.Tasks;
 4 
 5 namespace Example_15_1_3
 6 {
 7     internal class Program
 8     {
 9         [DllImport("Example_15_1_4.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
10         public extern static void InitData();
11 
12         public static Person person = new Person();
13 
14         static void Main(string[] args)
15         {
16             var code = person.GetHashCode();
17 
18             Task.Run(() =>
19             {
20                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Person 鎖");
21                 lock (person)
22                 {
23                     //調用C++
24                     InitData();
25                 }
26                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Person 鎖");
27             });
28             Console.ReadLine();
29         }
30     }
31 
32     public class Person { }
33 }
View Code

        1.4、Example_15_1_4(C++項目,動態庫類型(.dll))
 1 extern "C"
 2 {
 3     _declspec(dllexport) void InitData();
 4 }
 5 
 6 #include "iostream"
 7 #include <Windows.h>
 8 using namespace std;
 9 
10 void InitData()
11 {
12     printf("cpp 的業務邏輯 \n");
13 
14     auto handle = GetCurrentThread();
15 
16     TerminateThread(handle, 0);//退出線程
17 }
View Code

    2、眼見爲實
        
        2.1、我們手工調試 C# 程序的死鎖問題。
            項目源碼:Example_15_1_1
            這個項目不是使用通用的啓動方法,啓動過程是:我們編譯我們的項目,直接找到 EXE 程序,雙擊運行就可以了。等待我們的控制檯程序輸出:(tid=3,已經進入 Person(1111) 鎖)和(tid=4,已經進入 Student(22222) 鎖),沒有顯示有關(退出XXX)的字樣,說明程序死了。
            我們打開 Windbg,點擊【文件】---》【Attach to Process】附加進程,在右側的進程窗口,找到我們的項目【Example_15_1_1.exe】,點擊【Attach】附加,進入調試器界面,程序已經處於中斷狀態。
            我們使用【~0s】命令切換到主線程。
1 0:005> ~0s
2 eax=00000000 ebx=00000098 ecx=00000000 edx=00000000 esi=00fced24 edi=00000000
3 eip=774810fc esp=00fcec0c ebp=00fcec6c iopl=0         nv up ei pl nz na pe nc
4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
5 ntdll!NtReadFile+0xc:
6 774810fc c22400          ret     24h

            我們可以使用【!syncblk】命令查看一下是否我們程序有了什麼問題。

 1 0:000> !syncblk
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     8 01515e08            3         1 01525e60 1690   3   032d24d4 Example_15_1_1.Person
 4     9 01515e3c            3         1 01527688 f94   4   032d24e0 Example_15_1_1.Student
 5 -----------------------------
 6 Total           9
 7 CCW             1
 8 RCW             2
 9 ComClassFactory 0
10 Free            0

            我們這裏可以看到 3 號線程在持有 Person 對象,4 號線程在持有 Student 對象,然後我們分別依次切換到 3 號和 4號線程看看調用棧發生了什麼情況,我們先看看 3 好線程。            

1 0:000> ~3s
2 eax=00000000 ebx=00000001 ecx=00000000 edx=00000000 esi=00000001 edi=00000001
3 eip=7748166c esp=05cbec90 ebp=05cbee20 iopl=0         nv up ei pl nz ac pe nc
4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
5 ntdll!NtWaitForMultipleObjects+0xc:
6 7748166c c21400          ret     14h

            然後,我們使用【!clrstack】命令查看一下 3 號線程棧是什麼情況。

 1 0:003> !clrstack
 2 OS Thread Id: 0x1690 (3)
 3 Child SP       IP Call Site
 4 05cbefec 7748166c [GCFrame: 05cbefec] 
 5 05cbf0cc 7748166c [GCFrame: 05cbf0cc] 
 6 05cbf0e8 7748166c [HelperMethodFrame_1OBJ: 05cbf0e8] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
 7 05cbf164 6be28468 System.Threading.Monitor.Enter(System.Object, Boolean ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\monitor.cs @ 62]
 8 05cbf174 03270d53 Example_15_1_1.Program+c.b__2_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_15_1_1\Program.cs @ 19]
 9 05cbf1f0 6be8d4bb System.Threading.Tasks.Task.InnerInvoke() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884]
10 05cbf1fc 6be8b731 System.Threading.Tasks.Task.Execute() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498]
11 05cbf220 6be8b6fc System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861]
12 05cbf224 6be28604 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980]
13 05cbf290 6be28537 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928]
14 05cbf2a4 6be8b4b2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827]
15 05cbf308 6be8b357 System.Threading.Tasks.Task.ExecuteEntry(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767]
16 05cbf318 6be8b29d System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704]
17 05cbf31c 6bdfeb7d System.Threading.ThreadPoolWorkQueue.Dispatch() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820]
18 05cbf36c 6bdfe9db System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161]
19 05cbf58c 6d01f036 [DebuggerU2MCatchHandlerFrame: 05cbf58c] 

            紅色標註的行最有有一個數字19,這個數字就是表示代碼等待的行,可以去 Visual Studio 代碼中查找一下就知道了。

            我們繼續使用非託管命令【kb】查看一下。

 1 0:003> kb
 2  # ChildEBP RetAddr      Args to Child              
 3 00 05cbee20 75119623     00000001 01515e50 00000001 ntdll!NtWaitForMultipleObjects+0xc
 4 01 05cbee20 6d124461     00000001 01515e50 00000000 KERNELBASE!WaitForMultipleObjectsEx+0x103
 5 02 05cbee70 6d1240b0     00000000 ffffffff 00000001 clr!WaitForMultipleObjectsEx_SO_TOLERANT+0x3c
 6 03 05cbeef4 6d1241de     00000001 01515e50 00000000 clr!Thread::DoAppropriateWaitWorker+0x1eb
 7 04 05cbef60 6d124327     00000001 01515e50 00000000 clr!Thread::DoAppropriateWait+0x64
 8 05 05cbefac 6d03333b     ffffffff 00000001 00000000 clr!CLREventBase::WaitEx+0x121
 9 06 05cbefc4 6d10f1cb     ffffffff 00000001 00000000 clr!CLREventBase::Wait+0x1a
10 07 05cbf050 6d10f2fc     01525e60 ffffffff ebd8a514 clr!AwareLock::EnterEpilogHelper+0xa8
11 08 05cbf098 6d10f0d5     01525e60 ffffffff 032d24e0 clr!AwareLock::EnterEpilog+0x48
12 09 05cbf15c 6d141082     ebd8a4d0 032d24e0 05cbf1c0 clr!AwareLock::Enter+0x4a(Monitor 的底層就是 AwareLock)
13 0a 05cbf15c 6be28468     032d24ec 05cbf1dc 05cbf1e8 clr!JITutil_MonReliableEnter+0xb5
14 0b 05cbf16c 03270d53     00000000 00000000 00000000 mscorlib_ni!System.Threading.Monitor.Enter+0x18 [f:\dd\ndp\clr\src\BCL\system\threading\monitor.cs @ 62] 
15 0c 05cbf1e8 6be8d4bb     00000000 00000000 00000000 Example_15_1_1!Example_15_1_1.Program.<>c.<Main>b__2_0+0xbb [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_15_1_1\Program.cs @ 19] 
16 0d 05cbf1f4 6be8b731     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.InnerInvoke+0x4b [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884] 
17 0e 05cbf218 6be8b6fc     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.Execute+0x31 [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498] 
18 0f 05cbf280 6be28604     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.ExecutionContextCallback+0x1c [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861] 
19 10 05cbf280 6be28537     00000000 00000000 00000000 mscorlib_ni!System.Threading.ExecutionContext.RunInternal+0xc4 [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980] 
20 11 05cbf294 6be8b4b2     00000000 00000000 00000000 mscorlib_ni!System.Threading.ExecutionContext.Run+0x17 [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928] 
21 12 05cbf300 6be8b357     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.ExecuteWithThreadLocal+0xe2 [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827] 
22 13 05cbf310 6be8b29d     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.ExecuteEntry+0xb7 [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767] 
23 14 05cbf364 6bdfeb7d     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem+0xd [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704] 
24 15 05cbf364 6bdfe9db     00000000 00000000 00000000 mscorlib_ni!System.Threading.ThreadPoolWorkQueue.Dispatch+0x19d [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820] 
25 16 05cbf374 6d01f036     00000000 00000000 00000000 mscorlib_ni!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback+0xb [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161] 
26 17 05cbf374 6d0222da     05cbf408 05cbf3b8 6d1123d0 clr!CallDescrWorkerInternal+0x34
27 18 05cbf3c8 6d02859b     00000004 05cbf3f0 6d028731 clr!CallDescrWorkerWithHandler+0x6b
28 19 05cbf434 6d1cfe73     00000000 6bb99a5c 6bdfe9d0 clr!MethodDescCallSite::CallTargetWorker+0x16a
29 1a 05cbf4b4 6d1ce1e6     05cbf701 01525e60 05cbf5cc clr!QueueUserWorkItemManagedCallback+0x23
30 1b 05cbf4cc 6d1ce271     ebd8a0fc 00000001 05cbf5cc clr!ManagedThreadBase_DispatchInner+0x71
31 1c 05cbf570 6d1ce162     ebd8a048 00000001 01525e60 clr!ManagedThreadBase_DispatchMiddle+0x7e
32 1d 05cbf5c4 6d1ce351     00000001 00000000 00000001 clr!ManagedThreadBase_DispatchOuter+0x99
33 1e 05cbf5e8 6d1cfde9     00000001 00000004 ebd8a314 clr!ManagedThreadBase_FullTransitionWithAD+0x2f
34 1f 05cbf698 6d1cec23     05cbf703 05cbf701 01525e60 clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x102
35 20 05cbf714 6d1ce9d5     ebd8a298 6d1ce8b0 00000000 clr!ThreadpoolMgr::ExecuteWorkRequest+0x4f
36 21 05cbf714 6d0e4bb7     00000000 00000000 00000000 clr!ThreadpoolMgr::WorkerThreadStart+0x36c
37 22 05cbf838 7666f989     015182b0 7666f970 05cbf8a4 clr!Thread::intermediateThreadProc+0x58
38 23 05cbf848 77477084     015182b0 6f061da7 00000000 KERNEL32!BaseThreadInitThunk+0x19
39 24 05cbf8a4 77477054     ffffffff 7749629f 00000000 ntdll!__RtlUserThreadStart+0x2f
40 25 05cbf8b4 00000000     00000000 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b

            我們知道 Monitor 的底層是 AwareLock,如果我們對 AwareLock::Enter 很熟悉的話,(ebd8a4d0 032d24e0 05cbf1c0)這個三個數值就有我們想要的東西,第一個參數:ebd8a4d0 是 ecx,就是 this 的指針,第二參數:032d24e0,就是我們的鎖對象。然後,我們使用【!do】命令查看一下。

1 0:003> !do 032d24e0 
2 Name:        Example_15_1_1.Student
3 MethodTable: 01844e80
4 EEClass:     018413d4
5 Size:        12(0xc) bytes
6 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_15_1_1\bin\Debug\Example_15_1_1.exe
7 Fields:
8 None

            這裏就說明 3號線程等待的是 Student 對象鎖。4號線程持有 Student 對象,我們在切換到【~4s】4號線程看一看。

1 0:003> ~4s
2 eax=00000000 ebx=00000001 ecx=00000000 edx=00000000 esi=00000001 edi=00000001
3 eip=7748166c esp=05e7eee0 ebp=05e7f070 iopl=0         nv up ei pl nz ac po nc
4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
5 ntdll!NtWaitForMultipleObjects+0xc:
6 7748166c c21400          ret     14h

            我們也使用【!clrstack】命令查看一下 4號線程的調用棧,看看情況。

 1 0:004> !clrstack
 2 OS Thread Id: 0xf94 (4)
 3 Child SP       IP Call Site
 4 05e7f23c 7748166c [GCFrame: 05e7f23c] 
 5 05e7f31c 7748166c [GCFrame: 05e7f31c] 
 6 05e7f338 7748166c [HelperMethodFrame_1OBJ: 05e7f338] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
 7 05e7f3b4 6be28468 System.Threading.Monitor.Enter(System.Object, Boolean ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\monitor.cs @ 62]
 8 05e7f3c4 03270b73 Example_15_1_1.Program+c.b__2_1() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_15_1_1\Program.cs @ 34]
 9 05e7f440 6be8d4bb System.Threading.Tasks.Task.InnerInvoke() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884]
10 05e7f44c 6be8b731 System.Threading.Tasks.Task.Execute() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498]
11 05e7f470 6be8b6fc System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861]
12 05e7f474 6be28604 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980]
13 05e7f4e0 6be28537 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928]
14 05e7f4f4 6be8b4b2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827]
15 05e7f558 6be8b357 System.Threading.Tasks.Task.ExecuteEntry(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767]
16 05e7f568 6be8b29d System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704]
17 05e7f56c 6bdfeb7d System.Threading.ThreadPoolWorkQueue.Dispatch() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820]
18 05e7f5bc 6bdfe9db System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161]
19 05e7f7dc 6d01f036 [DebuggerU2MCatchHandlerFrame: 05e7f7dc] 

            標紅色的行最有一個數字34,就是表示在 IDE 中等待的代碼行。

            我們繼續使用【kb】命令查看一下。

 1 0:004> kb
 2  # ChildEBP RetAddr      Args to Child              
 3 00 05e7f070 75119623     00000001 01515e1c 00000001 ntdll!NtWaitForMultipleObjects+0xc
 4 01 05e7f070 6d124461     00000001 01515e1c 00000000 KERNELBASE!WaitForMultipleObjectsEx+0x103
 5 02 05e7f0c0 6d1240b0     00000000 ffffffff 00000001 clr!WaitForMultipleObjectsEx_SO_TOLERANT+0x3c
 6 03 05e7f144 6d1241de     00000001 01515e1c 00000000 clr!Thread::DoAppropriateWaitWorker+0x1eb
 7 04 05e7f1b0 6d124327     00000001 01515e1c 00000000 clr!Thread::DoAppropriateWait+0x64
 8 05 05e7f1fc 6d03333b     ffffffff 00000001 00000000 clr!CLREventBase::WaitEx+0x121
 9 06 05e7f214 6d10f1cb     ffffffff 00000001 00000000 clr!CLREventBase::Wait+0x1a
10 07 05e7f2a0 6d10f2fc     01527688 ffffffff ebf4a764 clr!AwareLock::EnterEpilogHelper+0xa8
11 08 05e7f2e8 6d10f0d5     01527688 ffffffff 032d24d4 clr!AwareLock::EnterEpilog+0x48
12 09 05e7f3ac 6d141082     ebf4a620 032d24d4 05e7f410 clr!AwareLock::Enter+0x4a
13 0a 05e7f3ac 6be28468     032d24ec 05e7f42c 05e7f438 clr!JITutil_MonReliableEnter+0xb5
14 0b 05e7f3bc 03270b73     00000000 00000000 00000000 mscorlib_ni!System.Threading.Monitor.Enter+0x18 [f:\dd\ndp\clr\src\BCL\system\threading\monitor.cs @ 62] 
15 0c 05e7f438 6be8d4bb     00000000 00000000 00000000 Example_15_1_1!Example_15_1_1.Program.<>c.<Main>b__2_1+0xbb [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_15_1_1\Program.cs @ 34] 
16 0d 05e7f444 6be8b731     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.InnerInvoke+0x4b [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884] 
17 0e 05e7f468 6be8b6fc     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.Execute+0x31 [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498] 
18 0f 05e7f4d0 6be28604     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.ExecutionContextCallback+0x1c [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861] 
19 10 05e7f4d0 6be28537     00000000 00000000 00000000 mscorlib_ni!System.Threading.ExecutionContext.RunInternal+0xc4 [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980] 
20 11 05e7f4e4 6be8b4b2     00000000 00000000 00000000 mscorlib_ni!System.Threading.ExecutionContext.Run+0x17 [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928] 
21 12 05e7f550 6be8b357     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.ExecuteWithThreadLocal+0xe2 [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827] 
22 13 05e7f560 6be8b29d     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.ExecuteEntry+0xb7 [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767] 
23 14 05e7f5b4 6bdfeb7d     00000000 00000000 00000000 mscorlib_ni!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem+0xd [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704] 
24 15 05e7f5b4 6bdfe9db     00000000 00000000 00000000 mscorlib_ni!System.Threading.ThreadPoolWorkQueue.Dispatch+0x19d [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820] 
25 16 05e7f5c4 6d01f036     00000000 00000000 00000000 mscorlib_ni!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback+0xb [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161] 
26 17 05e7f5c4 6d0222da     05e7f658 05e7f608 6d1123d0 clr!CallDescrWorkerInternal+0x34
27 18 05e7f618 6d02859b     00000004 05e7f640 6d028731 clr!CallDescrWorkerWithHandler+0x6b
28 19 05e7f684 6d1cfe73     00000000 6bb99a5c 6bdfe9d0 clr!MethodDescCallSite::CallTargetWorker+0x16a
29 1a 05e7f704 6d1ce1e6     05e7f951 01527688 05e7f81c clr!QueueUserWorkItemManagedCallback+0x23
30 1b 05e7f71c 6d1ce271     ebf4a24c 00000001 05e7f81c clr!ManagedThreadBase_DispatchInner+0x71
31 1c 05e7f7c0 6d1ce162     ebf4ad98 00000001 01527688 clr!ManagedThreadBase_DispatchMiddle+0x7e
32 1d 05e7f814 6d1ce351     00000001 00000000 00000001 clr!ManagedThreadBase_DispatchOuter+0x99
33 1e 05e7f838 6d1cfde9     00000001 00000004 ebf4ad64 clr!ManagedThreadBase_FullTransitionWithAD+0x2f
34 1f 05e7f8e8 6d1cec23     05e7f953 05e7f951 01527688 clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x102
35 20 05e7f964 6d1ce9d5     ebf4ace8 6d1ce8b0 00000000 clr!ThreadpoolMgr::ExecuteWorkRequest+0x4f
36 21 05e7f964 6d0e4bb7     00000000 00000202 05e7fb7c clr!ThreadpoolMgr::WorkerThreadStart+0x36c
37 22 05e7fafc 7666f989     01518460 7666f970 05e7fb68 clr!Thread::intermediateThreadProc+0x58
38 23 05e7fb0c 77477084     01518460 6f2a1e6b 00000000 KERNEL32!BaseThreadInitThunk+0x19
39 24 05e7fb68 77477054     ffffffff 7749629f 00000000 ntdll!__RtlUserThreadStart+0x2f
40 25 05e7fb78 00000000     00000000 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b

            clr!AwareLock::Enter 這個方法有三個參數,第一個參數:ebf4a620 是 this 指針,第二個參數:032d24d4 就是鎖對象。我們使用【!do】命令查看這個值。

1 0:004> !do 032d24d4
2 Name:        Example_15_1_1.Person
3 MethodTable: 01844e24
4 EEClass:     01841380
5 Size:        12(0xc) bytes
6 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_15_1_1\bin\Debug\Example_15_1_1.exe
7 Fields:
8 None

            4 號線程鎖住的 Person 對象,3 號線程在等待這個 Person 對象,3 號線程鎖住了 Student 對象,4號線程又在等待這個被鎖住的對象,就是這樣,死鎖就發生了。


        2.2、我們看看孤兒鎖是如何發生的。
            項目源碼:Example_15_1_2
            這個項目不是使用通用的啓動方法,啓動過程是:我們編譯我們的項目,直接找到 EXE 程序,雙擊運行就可以了。
            執行效果:
              

              上圖說的很清楚,也就是我們代碼中,這個行代碼【Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Person(222222) ");】沒有執行,爲什麼呢?因爲卡在了【Monitor.Enter(person);】這裏,效果如圖:
              

            以上就說明我們的程序卡死了。
            我們打開 Windbg,點擊【文件】---》【Attach to Process】附加進程,在右側的進程窗口,找到我們的項目【Example_15_1_2.exe】,點擊【Attach】附加,進入調試器界面,程序已經處於中斷狀態。我們需要使用切換到主線程,執行命令【~0s】,屏幕內容太多,在清理一下屏幕,執行命令【.cls】。
1 0:005> ~0s
2 eax=00000000 ebx=00000098 ecx=00000000 edx=00000000 esi=00cff154 edi=00000000
3 eip=774810fc esp=00cff03c ebp=00cff09c iopl=0         nv up ei pl nz na pe nc
4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
5 ntdll!NtReadFile+0xc:
6 774810fc c22400          ret     24h

            我們使用【!syncblk】命令查看一下。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     8 01040a90            3         1 01052638 0 XXX   02cb24d4 Example_15_1_2.Person
4 -----------------------------
5 Total           21
6 CCW             1
7 RCW             9
8 ComClassFactory 0
9 Free            0

            01052638 這個值就是 CLR 裏面線程的結構,我們可以使用【dp】命令證明一下。

1 0:000> dp 01052638
2 01052638  6d0e4bd4 01039820 00000000 ffffffff
3 01052648  00000000 00fdfd78 00000001 00000003
4 01052658  0105265c 0105265c 0105265c 00000000
5 01052668  00000000 00000000 00fc9db8 00a5e000
6 01052678  00000000 00000000 00000000 00000000
7 01052688  00000000 00000000 00000000 00000000
8 01052698  00000000 00000000 6ba86044 0104f1d0
9 010526a8  01062ca8 01062cb0 00000200 01062ca8

            00000003 這個值這個對象包含的是3號線程。我們可以使用【!t】命令佐證一下。

 1 0:000> !t
 2 ThreadCount:      4
 3 UnstartedThread:  0
 4 BackgroundThread: 2
 5 PendingThread:    0
 6 DeadThread:       1
 7 Hosted Runtime:   no
 8                                                                          Lock  
 9        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
10    0    1 3bac 00fe6b48     2a020 Preemptive  02CBA254:00000000 00fdfd78 1     MTA 
11    2    2 1d70 01024528     2b220 Preemptive  00000000:00000000 00fdfd78 0     MTA (Finalizer) 
12 XXXX    3    0 01052638   1039820 Preemptive  00000000:00000000 00fdfd78 1     Ukn (Threadpool Worker) 
13    3    4  db4 01056f70   3029220 Preemptive  02CB8410:00000000 00fdfd78 0     MTA (Threadpool Worker) 

            紅色標註的就是3號線程。3號線程的 OSID 的值是0,線程棧也看不到了,表示操作系統的線程對象已經銷燬了,所以前面才顯示 XXXX。


        2.3、和【非託管代碼】交互式的鎖問題。
            項目源碼:Example_15_1_3 和 Example_15_1_4(C++)
            這個項目不是使用通用的啓動方法,啓動過程是:我們編譯我們的項目,直接找到 EXE 程序,雙擊運行就可以了。等待我們的控制檯程序輸出:(tid=3,已經進入 Person 鎖)和(cpp 的業務邏輯),執行效果如圖:
            
            我們打開 Windbg,點擊【文件】---》【Attach to Process】附加進程,在右側的進程窗口,找到我們的項目【Example_15_1_3.exe】,點擊【Attach】附加,進入調試器界面,程序已經處於中斷狀態。
            我們首先切換到主線程【~0s】,然後再繼續執行。
1 0:010> ~0s
2 eax=00000000 ebx=00000098 ecx=00000000 edx=00000000 esi=00bfef2c edi=00000000
3 eip=774810fc esp=00bfee14 ebp=00bfee74 iopl=0         nv up ei pl nz na pe nc
4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
5 ntdll!NtReadFile+0xc:
6 774810fc c22400          ret     24h

            我們在使用【!syncblk】命令查看一下同步塊表。

1 0:000> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
3     6 0000018f54102168            1         1 0000018f54113160 3274 XXX   0000018f54262ea8 Example_15_1_3.Person
4 -----------------------------
5 Total           6
6 CCW             1
7 RCW             2
8 ComClassFactory 0
9 Free            0

            我們看到了XXX,就知道發生了不好的事。我們可以看看當前的線程列表,使用【!t】命令。

 1 0:000> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 2
 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 2514 0000018f527fdd40    2a020 Preemptive  0000018F5426B1C0:0000018F5426BFD0 0000018f52785030 1     MTA 
11    2    2 1c70 0000018f527900b0    2b220 Preemptive  0000000000000000:0000000000000000 0000018f52785030 0     MTA (Finalizer) 
12 XXXX    3 3274 0000018f54113160  1029220 Preemptive  0000018F54269870:0000018F54269FD0 0000018f52785030 1     Ukn (Threadpool Worker) 

            我們看到紅色標記的 XXXX 號線程,可以點擊一下OSID是 3274,嘗試切換一下線程。

1 0:000> ~~[3274]s
2               ^ Illegal thread error in '~~[3274]s'

            是非法線程,已經銷燬了。lock 鎖如果在託管環境中是可以完成釋放的,如果和非託管代碼有交互,線程有跨界,可能就會有問題,切記。


四、總結
    終於寫完了。還是老話,雖然很忙,寫作過程也挺累的,但是看到了自己的成長,心裏還是挺快樂的。學習過程真的沒那麼輕鬆,還好是自己比較喜歡這一行,否則真不知道自己能不能堅持下來。老話重談,《高級調試》的這本書第一遍看,真的很暈,第二遍稍微好點,不學不知道,一學嚇一跳,自己欠缺的很多。好了,不說了,不忘初心,繼續努力,希望老天不要辜負努力的人。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章