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


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

二、基础知识
    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

 View Code


        1.2、Example_14_1_2

 View Code


        1.3、Example_14_1_3

 View Code


        1.4、Example_14_1_4(Net 7.0)

 View Code


        1.5、Example_14_1_5

 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】命令查看一下详情。

 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
复制代码

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统备份用户数据  123
统信桌面专业版【全盘安装UOS系统】介绍  117
银河麒麟桌面操作系统安装佳能打印机驱动方法  109
银河麒麟桌面操作系统 V10-SP1用户密码修改  102
最近下载排行榜
银河麒麟桌面操作系统备份用户数据 0
统信桌面专业版【全盘安装UOS系统】介绍 0
银河麒麟桌面操作系统安装佳能打印机驱动方法 0
银河麒麟桌面操作系统 V10-SP1用户密码修改 0
作者收入月榜
1

prtyaa 收益393.62元

2

zlj141319 收益218元

3

1843880570 收益214.2元

4

IT-feng 收益209.03元

5

风晓 收益208.24元

6

777 收益172.71元

7

Fhawking 收益106.6元

8

信创来了 收益105.84元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.5元

请使用微信扫码

加入交流群

请使用微信扫一扫!