Net 高级调试之十五:经典的锁故障

发布时间 2023-12-28 10:27:11作者: 可均可可
一、简介
    今天是《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 锁如果在托管环境中是可以完成释放的,如果和非托管代码有交互,线程有跨界,可能就会有问题,切记。


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