一、简介
    今天是《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 }

        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 }

        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 }

        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 }

    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 锁如果在托管环境中是可以完成释放的,如果和非托管代码有交互,线程有跨界,可能就会有问题,切记。

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

Net 高级调试之十五:经典的锁故障的更多相关文章

  1. JAVA基础再回首(二十五)——Lock锁的使用、死锁问题、多线程生产者和消费者、线程池、匿名内部类使用多线程、定时器、面试题

    JAVA基础再回首(二十五)--Lock锁的使用.死锁问题.多线程生产者和消费者.线程池.匿名内部类使用多线程.定时器.面试题 版权声明:转载必须注明本文转自程序猿杜鹏程的博客:http://blog ...

  2. 前端 高级 (二十五)vue2.0项目实战一 配置简要说明、代码简要说明、Import/Export、轮播和列表例子

    一.启动服务自动打开浏览器运行 二.配置简要说明 1.node_modules 安装好的依赖文件,中间件等,所在位置 2.package.jason 配置当前项目要安装的中间件和依赖文件 { &quo ...

  3. MySQL高级知识(十五)——主从复制

    前言:本章主要讲解MySQL主从复制的操作步骤.由于环境限制,主机使用Windows环境,从机使用用Linux环境.另外MySQL的版本最好一致,笔者采用的MySQL5.7.22版本,具体安装过程请查 ...

  4. 读书笔记 - js高级程序设计 - 第十五章 使用Canvas绘图

    读书笔记 - js高级程序设计 - 第十三章 事件   canvas 具备绘图能力的2D上下文 及文本API 很多浏览器对WebGL的3D上下文支持还不够好   有时候即使浏览器支持,操作系统如果缺缺 ...

  5. C#高级编程 第十五章 反射

    (二)自定义特性 使自定义特性非常强大的因素时使用反射,代码可以读取这些元数据,使用它们在运行期间作出决策. 1.编写自定义特性 定义一个FieldName特性: [AttributeUsage(At ...

  6. C#高级编程七十五天----C#使用指针

    在C#中使用指针的语法 假设想在C#中使用指针,首先对项目进行过配置: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/font ...

  7. 【读书笔记】C#高级编程 第二十五章 事务处理

    (一)简介 事务的主要特征是,任务要么全部完成,要么都不完成. (二)概述 事务由事务管理器来管理和协调.每个影响事务结果的资源都由一个资源管理器来管理.事务管理器与资源管理器通信,以定义事务的结果. ...

  8. 【读书笔记】C#高级编程 第十五章 反射

    (一)在运行期间处理和检查代码 自定义特性允许把自定义元数据与程序元素关联起来.反射是一个普通术语,它描述了在运行过程中检查和处理程序元素的功能.例如,反射允许完成的任务: 枚举类型的成员 实例化新对 ...

  9. MySQL高级知识(十四)——行锁

    前言:前面学习了表锁的相关知识,本篇主要介绍行锁的相关知识.行锁偏向InnoDB存储引擎,开销大,加锁慢,会出现死锁,锁定粒度小,发生锁冲突的概率低,但并发度高. 0.准备 #1.创建相关测试表tb_ ...

  10. Python进阶(三十五)-Fiddler命令行和HTTP断点调试

    Python进阶(三十五)-Fiddler命令行和HTTP断点调试 一. Fiddler内置命令   上一节(使用Fiddler进行抓包分析)中,介绍到,在web session(与我们通常所说的se ...

随机推荐

  1. jQuery默认选中下拉框的某个值

    $("#quaterSelect").val("0");//id为quaterSelect的下拉框默认选中value是0的option选项

  2. Hutool,一个超好用的 Java 工具类库

    一.简介 Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以"甜甜的". ...

  3. 「shoi 2012」随机数

    link. 对于 pass 1, 你把他考虑成 \(\frac{\sum x}{i}\) 的形式, 于是每次操作的贡献就是 \(\frac{2}{i}\), 那么答案就是 \(\sum_{i=2}^n ...

  4. 解决软件安装无法自定义文件夹,自动安装在C盘 (Windows系统)

    其实就是软链接的简单应用 1.软件已经自动安装 2.完全退出当前软件 3.通过软件图标的属性找到其实际的安装目录 4.进入该软件的安装目录 5.将该软件整个剪切(你没有看错)到指定文件夹(自定义的安装 ...

  5. 使用ensp搭建路由拓扑,并使用ospf协议实现网络互通实操

    转载请注明出处: 1.使用ENSP 搭建如下拓扑: 数据准备 为完成此配置例,需准备如下的数据: 设备 Router ID Process ID IP地址 DeviceA 1.1.1.1 1 区域0: ...

  6. CCF CSP认证注册、报名、查询成绩、做模拟题等答疑

    CCF CSP认证注册.报名.查询成绩.做模拟题等答疑 CCF CSP认证中心将考生在注册,或报名,或查询成绩,或历次真题练习时遇到的问题进行汇总,并给出解决方法,具体如下: 1.注册时,姓名可否随意 ...

  7. win11系统无法解决的死结

    如果需要使用网上银行.win11一定不能使用. win11已经取消了,对于IE浏览器的支持和安装. 但是大部分网银都是要求IE浏览器.或者IE内核.实际过程当中.虽然所有的浏览器都说兼容IE有IE内核 ...

  8. 【Unity3D】UI Toolkit样式选择器

    1 前言 ​ UI Toolkit简介 中介绍了样式属性,UI Toolkit容器 和 UI Toolkit元素 中介绍了容器和元素,本文将介绍样式选择器(Selector),主要包含样式类选择器(C ...

  9. 文心一言 VS 讯飞星火 VS chatgpt (119)-- 算法导论10.3 4题

    四.用go语言,我们往往希望双向链表的所有元素在存储器中保持紧凑,例如,在多数组表示中占用前m 个下标位置.(在页式虚拟存储的计算环境下,即为这种情况.)假设除指向链表本身的指针外没有其他指针指向该链 ...

  10. 安装 Android x86 并开启 arm 兼容

    安装 Android x86 并开启 arm 兼容 Win 11 下开启了 Hyper-v,尝试了各种安卓模拟器,要么不能设置代理(BlueStacks),要么/system目录没办法设置. 获取 A ...