一、介绍
    这是我的《Advanced .Net Debugging》这个系列的第八篇文章。这篇文章的内容是原书的第二部分的【调试实战】的第六章【同步】。我们经常写一些多线程的应用程序,写的多了,有关多线程的问题出现的也就多了,因此,最迫切的任务就是提高解决多线程同步问题的能力。这一节我们将从本质上、从底层上来介绍线程的同步组件和同步原理,也会给出在多线程环境下如何解决问题的最佳实践。高级调试会涉及很多方面的内容,你对 .NET 基础知识掌握越全面、细节越底层,调试成功的几率越大,当我们遇到各种奇葩问题的时候才不会手足无措。
    如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

     调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
          操作系统:Windows Professional 10
          调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
          下载地址:可以去Microsoft Store 去下载
          开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
          Net 版本:.Net 8.0
          CoreCLR源码:源码下载

    
在此说明:我使用了两种调试工具,第一种:Windbg Preivew,图形界面,使用方便,操作顺手,不用担心干扰;第二种是:NTSD,是命令行形式的调试器,在命令使用上和 Windbg 没有任何区别,之所以增加了它的调试过程,不过是我的个人爱好,想多了解一些,看看他们有什么区别,为了学习而使用的。如果在工作中,我推荐使用 Windbg Preview,更好用,更方便,也不会出现奇怪问题(我在使用 NTSD 调试断点的时候,不能断住,提示内存不可读,Windbg preview 就没有任何问题)。
    
如果大家想了解调试过程,二选一即可,当然,我推荐查看【Windbg Preview 调试】。

二、目录结构
    为了让大家看的更清楚,也为了自己方便查找,我做了一个目录结构,可以直观的查看文章的布局、内容,可以有针对性查看。
    1、同步的基础知识
        A、基础知识
        B、眼见为实
            1)、KD 和 NTSD 调试
            2)、Windbg Preview 调试
    2、线程同步原语
        2.1、事件同步原语(内核锁)
            A、基础知识
            B、眼见为实
                1)、KD 和 NTSD 调试
                2)、Windbg Preview 调试
        2.2、互斥体(内核锁)
            A、基础知识
            B、眼见为实
                1)、KD 和 NTSD 调试
                2)、Windbg Preview 调试
        2.3、信号量(内核锁)
            A、基础知识
            B、眼见为实
                1)、KD 和 NTSD 调试
                2)、Windbg Preview 调试
        2.4、监视器
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
        2.5、读写锁
            A、基础知识
            B、眼见为实
                1)、NTSD  调试
                2)、Windbg Preview 调试
        2.6、线程池
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
    3、同步的内部细节
        3.1、对象头
        3.2、同步块
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
        3.3、瘦锁
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
    4、同步任务
        4.1、死锁
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
        4.2、孤立锁:异常
            A、基础知识
            B、眼见为实
                1)、NTDS 调试
                2)、Windbg Preview 调试
        4.3、线程中止
        4.4、终结器挂起

三、调试源码
    废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
    3.1、ExampleCore_6_1

 1 namespace ExampleCore_6_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var thread = new Thread(() =>
 8             {
 9                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId}");
10                 Console.ReadLine();
11             });
12 
13             thread.Start();
14 
15             Console.ReadLine();
16         }
17     }
18 }
View Code

    3.2、ExampleCore_6_2

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_2
 4 {
 5     internal class Program
 6     {        
 7         static void Main(string[] args)
 8         {            
 9             while (true)
10             {
11                 Console.WriteLine("选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)");
12                 var myword= Console.ReadLine();
13                 if (string.Compare(myword, "Manual", true) == 0)
14                 {
15                     RunManualResetEvent();
16                 }
17                 else if (string.Compare(myword, "Auto", true) == 0)
18                 {
19                     RunAutoResetEvent();
20                 }
21                 else if (string.Compare(myword, "Exit", true) == 0)
22                 {
23                     break;
24                 }
25             }
26         }
27 
28         static void RunManualResetEvent()
29         {
30             ManualResetEvent? mre = new ManualResetEvent(false);
31 
32             Console.WriteLine($"mre 默认为 false,即等待状态,请查看!");
33             Debugger.Break();
34 
35             mre.Set();
36             Console.WriteLine($"mre 默认为 true,即放行状态,请查看!");
37             Debugger.Break();
38 
39             mre.Reset();
40             Console.WriteLine($"mre Reset 后为 false,即等待状态,请查看!");
41             Debugger.Break();
42 
43             mre = null;
44         }
45 
46         static void RunAutoResetEvent()
47         {
48             AutoResetEvent? mre = new AutoResetEvent(false);
49 
50             Console.WriteLine($"are 默认为 false,即等待状态,请查看!");
51             Debugger.Break();
52 
53             mre.Set();
54             Console.WriteLine($"are 默认为 true,即放行状态,请查看!");
55             Debugger.Break();
56 
57             mre.Reset();
58             Console.WriteLine($"are Reset 后为 false,即等待状态,请查看!");
59             Debugger.Break();
60 
61             mre = null;
62         }
63     }
64 }
View Code

    3.3、ExampleCore_6_3

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_3
 4 {
 5     internal class Program
 6     {
 7         private static Mutex mut = new Mutex();
 8 
 9         static void Main()
10         {
11             UseResource();
12         }
13 
14         private static void UseResource()
15         {
16             // 等到安全进入。
17             mut.WaitOne();
18 
19             Console.WriteLine("已进入保护区");
20 
21             Debugger.Break();
22 
23             Console.WriteLine("正在离开保护区");
24 
25             // 释放互斥锁。
26             mut.ReleaseMutex();
27 
28             Debugger.Break();
29         }
30     }
31 }
View Code

    3.4、ExampleCore_6_4

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_4
 4 {
 5     internal class Program
 6     {
 7         public static Semaphore sem = new Semaphore(1, 10);
 8         static void Main(string[] args)
 9         {
10             for (int i = 0; i < int.MaxValue; i++)
11             {
12                 sem.Release();
13                 Console.WriteLine("查看当前的 sem 值。");
14                 Debugger.Break();
15             }
16         }
17     }
18 }
View Code

    3.5、ExampleCore_6_5

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_5
 4 {
 5     internal class Program
 6     {
 7         public static Person person = new Person();
 8 
 9         static void Main(string[] args)
10         {
11             Task.Run(() =>
12             {
13                 lock (person)
14                 {
15                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中 111111");
16                     Debugger.Break();
17                 }
18             });
19             Task.Run(() =>
20             {
21                 lock (person)
22                 {
23                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中 222222");
24                     Debugger.Break();
25                 }
26             });
27             Console.ReadLine();
28         }
29     }
30 
31     public class Person
32     {
33     }
34 }
View Code

    3.6、ExampleCore_6_6

  1 namespace ExampleCore_6_6
  2 {
  3     internal class Program
  4     {
  5         private static ReaderWriterLock rwl = new ReaderWriterLock();
  6         // Define the shared resource protected by the ReaderWriterLock.
  7         static int resource = 0;
  8 
  9         const int numThreads = 1;
 10         static bool running = true;
 11 
 12         // Statistics.
 13         static int readerTimeouts = 0;
 14         static int writerTimeouts = 0;
 15         static int reads = 0;
 16         static int writes = 0;
 17 
 18         static void Main(string[] args)
 19         {
 20             Thread[] t = new Thread[numThreads];
 21             for (int i = 0; i < numThreads; i++)
 22             {
 23                 t[i] = new Thread(new ThreadStart(ThreadProc));
 24                 t[i].Name = new String((char)(i + 65), 1);
 25                 t[i].Start();
 26                 if (i > 10)
 27                     Thread.Sleep(300);
 28             }
 29 
 30             // Tell the threads to shut down and wait until they all finish.
 31             running = false;
 32             for (int i = 0; i < numThreads; i++)
 33                 t[i].Join();
 34 
 35             // Display statistics.
 36             Console.WriteLine("\n{0} reads, {1} writes, {2} reader time-outs, {3} writer time-outs.",
 37                   reads, writes, readerTimeouts, writerTimeouts);
 38             Console.Write("Press ENTER to exit... ");
 39             Console.ReadLine();
 40         }
 41 
 42         static void ThreadProc()
 43         {
 44             Random rnd = new Random();
 45 
 46             // Randomly select a way for the thread to read and write from the shared
 47             // resource.
 48             while (running)
 49             {
 50                 double action = rnd.NextDouble();
 51                 if (action < .8)
 52                     ReadFromResource(10);
 53                 else if (action < .81)
 54                     ReleaseRestore(rnd, 50);
 55                 else if (action < .90)
 56                     UpgradeDowngrade(rnd, 100);
 57                 else
 58                     WriteToResource(rnd, 100);
 59             }
 60         }
 61 
 62         // Request and release a reader lock, and handle time-outs.
 63         static void ReadFromResource(int timeOut)
 64         {
 65             try
 66             {
 67                 rwl.AcquireReaderLock(timeOut);
 68                 try
 69                 {
 70                     // It is safe for this thread to read from the shared resource.
 71                     Display("reads resource value " + resource);
 72                     Interlocked.Increment(ref reads);
 73                 }
 74                 finally
 75                 {
 76                     // Ensure that the lock is released.
 77                     rwl.ReleaseReaderLock();
 78                 }
 79             }
 80             catch (ApplicationException)
 81             {
 82                 // The reader lock request timed out.
 83                 Interlocked.Increment(ref readerTimeouts);
 84             }
 85         }
 86 
 87         // Request and release the writer lock, and handle time-outs.
 88         static void WriteToResource(Random rnd, int timeOut)
 89         {
 90             try
 91             {
 92                 rwl.AcquireWriterLock(timeOut);
 93                 try
 94                 {
 95                     // It's safe for this thread to access from the shared resource.
 96                     resource = rnd.Next(500);
 97                     Display("writes resource value " + resource);
 98                     Interlocked.Increment(ref writes);
 99                 }
100                 finally
101                 {
102                     // Ensure that the lock is released.
103                     rwl.ReleaseWriterLock();
104                 }
105             }
106             catch (ApplicationException)
107             {
108                 // The writer lock request timed out.
109                 Interlocked.Increment(ref writerTimeouts);
110             }
111         }
112 
113         // Requests a reader lock, upgrades the reader lock to the writer
114         // lock, and downgrades it to a reader lock again.
115         static void UpgradeDowngrade(Random rnd, int timeOut)
116         {
117             try
118             {
119                 rwl.AcquireReaderLock(timeOut);
120                 try
121                 {
122                     // It's safe for this thread to read from the shared resource.
123                     Display("reads resource value " + resource);
124                     Interlocked.Increment(ref reads);
125 
126                     // To write to the resource, either release the reader lock and
127                     // request the writer lock, or upgrade the reader lock. Upgrading
128                     // the reader lock puts the thread in the write queue, behind any
129                     // other threads that might be waiting for the writer lock.
130                     try
131                     {
132                         LockCookie lc = rwl.UpgradeToWriterLock(timeOut);
133                         try
134                         {
135                             // It's safe for this thread to read or write from the shared resource.
136                             resource = rnd.Next(500);
137                             Display("writes resource value " + resource);
138                             Interlocked.Increment(ref writes);
139                         }
140                         finally
141                         {
142                             // Ensure that the lock is released.
143                             rwl.DowngradeFromWriterLock(ref lc);
144                         }
145                     }
146                     catch (ApplicationException)
147                     {
148                         // The upgrade request timed out.
149                         Interlocked.Increment(ref writerTimeouts);
150                     }
151 
152                     // If the lock was downgraded, it's still safe to read from the resource.
153                     Display("reads resource value " + resource);
154                     Interlocked.Increment(ref reads);
155                 }
156                 finally
157                 {
158                     // Ensure that the lock is released.
159                     rwl.ReleaseReaderLock();
160                 }
161             }
162             catch (ApplicationException)
163             {
164                 // The reader lock request timed out.
165                 Interlocked.Increment(ref readerTimeouts);
166             }
167         }
168 
169         // Release all locks and later restores the lock state.
170         // Uses sequence numbers to determine whether another thread has
171         // obtained a writer lock since this thread last accessed the resource.
172         static void ReleaseRestore(Random rnd, int timeOut)
173         {
174             int lastWriter;
175 
176             try
177             {
178                 rwl.AcquireReaderLock(timeOut);
179                 try
180                 {
181                     // It's safe for this thread to read from the shared resource,
182                     // so read and cache the resource value.
183                     int resourceValue = resource;     // Cache the resource value.
184                     Display("reads resource value " + resourceValue);
185                     Interlocked.Increment(ref reads);
186 
187                     // Save the current writer sequence number.
188                     lastWriter = rwl.WriterSeqNum;
189 
190                     // Release the lock and save a cookie so the lock can be restored later.
191                     LockCookie lc = rwl.ReleaseLock();
192 
193                     // Wait for a random interval and then restore the previous state of the lock.
194                     Thread.Sleep(rnd.Next(250));
195                     rwl.RestoreLock(ref lc);
196 
197                     // Check whether other threads obtained the writer lock in the interval.
198                     // If not, then the cached value of the resource is still valid.
199                     if (rwl.AnyWritersSince(lastWriter))
200                     {
201                         resourceValue = resource;
202                         Interlocked.Increment(ref reads);
203                         Display("resource has changed " + resourceValue);
204                     }
205                     else
206                     {
207                         Display("resource has not changed " + resourceValue);
208                     }
209                 }
210                 finally
211                 {
212                     // Ensure that the lock is released.
213                     rwl.ReleaseReaderLock();
214                 }
215             }
216             catch (ApplicationException)
217             {
218                 // The reader lock request timed out.
219                 Interlocked.Increment(ref readerTimeouts);
220             }            
221         }
222 
223         // Helper method briefly displays the most recent thread action.
224             static void Display(string msg)
225             {
226                 Console.Write("Thread {0} {1}.       \r", Thread.CurrentThread.Name, msg);
227             }
228     }
229 }
View Code

    3.7、ExampleCore_6_6_ThreadPool

 1 namespace ExampleCore_6_6_ThreadPool
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             ThreadPool.GetMaxThreads(out int workderThreads, out int completionPortThreads);
 8             Console.WriteLine($"修改前-GetMaxThreads:workderThreads={workderThreads},completionPortThreads={completionPortThreads}");
 9 
10             ThreadPool.GetMinThreads(out int workderThreads2, out int completionPortThreads2);
11             Console.WriteLine($"修改前-GetMinThreads:workderThreads={workderThreads2},completionPortThreads={completionPortThreads2}");
12 
13             ThreadPool.SetMinThreads(0, 0);
14 
15             ThreadPool.GetMinThreads(out int workderThreads3, out int completionPortThreads3);
16             Console.WriteLine($"修改后-SetMinThreads-GetMinThreads:workderThreads={workderThreads3},completionPortThreads={completionPortThreads3}");
17 
18             ThreadPool.SetMaxThreads(32769, 32769);
19             ThreadPool.GetMaxThreads(out int workderThreads4, out int completionPortThreads4);
20             Console.WriteLine($"修改后-SetMaxThreads-GetMaxThreads:workderThreads={workderThreads4},completionPortThreads={completionPortThreads4}");
21 
22             ThreadPool.QueueUserWorkItem(o => {
23                 Console.WriteLine("ThreadPool.QueueUserWorkItem 开始执行");
24                 while (true)
25                 {
26                 }
27             });
28 
29             Task.Run(() => {
30                 Console.WriteLine("Task.Run 开始执行");
31                 while (true)
32                 {
33                 }
34             });
35 
36             Console.ReadLine();
37         }
38     }
39 }
View Code

    3.8、ExampleCore_6_6_ThreadPool.Net(Net Framework 4.8)

 1 using System;
 2 using System.Threading;
 3 using System.Threading.Tasks;
 4 
 5 namespace ExampleCore_6_6_ThreadPool.Net
 6 {
 7     internal class Program
 8     {
 9         static void Main(string[] args)
10         {
11             ThreadPool.GetMaxThreads(out int workderThreads, out int completionPortThreads);
12             Console.WriteLine($"修改前-GetMaxThreads:workderThreads={workderThreads},completionPortThreads={completionPortThreads}");
13 
14             ThreadPool.GetMinThreads(out int workderThreads2, out int completionPortThreads2);
15             Console.WriteLine($"修改前-GetMinThreads:workderThreads={workderThreads2},completionPortThreads={completionPortThreads2}");
16 
17             ThreadPool.SetMinThreads(-1, 0);
18 
19             ThreadPool.GetMinThreads(out int workderThreads3, out int completionPortThreads3);
20             Console.WriteLine($"修改后-SetMinThreads-GetMinThreads:workderThreads={workderThreads3},completionPortThreads={completionPortThreads3}");
21 
22             ThreadPool.SetMaxThreads(32765, 32765);
23             ThreadPool.GetMaxThreads(out int workderThreads4, out int completionPortThreads4);
24             Console.WriteLine($"修改后-SetMaxThreads-GetMaxThreads:workderThreads={workderThreads4},completionPortThreads={completionPortThreads4}");
25 
26             ThreadPool.QueueUserWorkItem(o => {
27                 Console.WriteLine("ThreadPool.QueueUserWorkItem 开始执行");
28                 while (true)
29                 {
30                 }
31             });
32 
33             Task.Run(() => {
34                 Console.WriteLine("Task.Run 开始执行");
35                 while (true)
36                 {
37                 }
38             });
39 
40             Console.ReadLine();
41         }
42     }
43 }
View Code


    3.9、ExampleCore_6_7

 1 namespace ExampleCore_6_7
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Program program = new Program();
 8             program.Run();
 9         }
10 
11         public void Run()
12         {
13             var mycode = GetHashCode();
14             Console.WriteLine("HashCode:" + mycode);
15 
16             Console.WriteLine("Press any key to acquire lock");
17             Console.ReadLine();
18 
19             Monitor.Enter(this);
20 
21             Console.WriteLine("Press any key to release lock");
22             Console.ReadLine();
23 
24             Monitor.Exit(this);
25 
26             Console.WriteLine("Press any key to Exit");
27             Console.ReadLine();
28         }
29     }
30 }
View Code

    3.10、ExampleCore_6_8

 1 namespace ExampleCore_6_8
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Program program = new Program();
 8             program.Run();
 9         }
10 
11         public void Run()
12         {            
13             Console.WriteLine("Press any key to acquire lock");
14             Console.ReadLine();
15 
16             Monitor.Enter(this);
17 
18             Console.WriteLine("Press any key to get hashcode");
19             Console.ReadLine();
20 
21             var mycode = GetHashCode();
22             Console.WriteLine("HashCode:" + mycode);
23 
24             Console.WriteLine("Press any key to release lock");
25             Console.ReadLine();
26 
27             Monitor.Exit(this);
28 
29             Console.WriteLine("Press any key to Exit");
30             Console.ReadLine();
31         }
32     }
33 }
View Code

    3.11、ExampleCore_6_9

 1 namespace ExampleCore_6_9
 2 {
 3     internal class Program
 4     {
 5         public static Person person = new Person();
 6         public static Student student = new Student();
 7         static void Main(string[] args)
 8         {
 9             Task.Run(() =>
10             {
11                 lock (person)
12                 {
13                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Person(1111) 锁");
14                     Thread.Sleep(1000);
15                     lock (student)
16                     {
17                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Student(1111) 锁");
18                         Console.ReadLine();
19                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经退出 Student(1111) 锁");
20                     }
21                 }
22             });
23 
24             Task.Run(() =>
25             {
26                 lock (student)
27                 {
28                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Student(22222) 锁");
29                     Thread.Sleep(1000);
30                     lock (person)
31                     {
32                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Person(22222) 锁");
33                         Console.ReadLine();
34                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经退出 Person(22222) 锁");
35                     }
36                 }
37             });
38 
39             Console.ReadLine();
40         }
41     }
42 
43     public class Student { }
44 
45     public class Person { }
46 }
View Code
    3.12、ExampleCore_6_10
 1 namespace ExampleCore_6_10
 2 {
 3     internal class DBWrapper
 4     {
 5         private string _connectionString;
 6 
 7         public DBWrapper(string connectionString)
 8         {
 9             _connectionString = connectionString;
10         }
11     }
12 
13     internal class Program
14     {
15         private static DBWrapper? dBWrapper;
16 
17         static void Main(string[] args)
18         {
19             dBWrapper = new DBWrapper("DB1");
20 
21             Thread thread = new Thread(ThreadProc);
22             thread.Start();
23 
24             Thread.Sleep(500);
25 
26             Console.WriteLine("Acquiring Lock!");
27             Monitor.Enter(dBWrapper);
28 
29             Thread.Sleep(2000);
30 
31             Console.WriteLine("Releasing Lock!");
32             Monitor.Exit(dBWrapper);
33         }
34 
35         private static void ThreadProc()
36         {
37             try
38             {
39                 Monitor.Enter(dBWrapper!);
40                 Call3rdPartyCode(null);
41                 Monitor.Exit(dBWrapper!);
42             }
43             catch (Exception)
44             {
45                 Console.WriteLine("3rd party code throw an exception");
46             }
47         }
48 
49         private static void Call3rdPartyCode(object? obj)
50         {
51             if (obj == null)
52             {
53                 throw new NullReferenceException();
54             }
55         }
56     }
57 }
View Code

    3.13、ExampleCore_6_10.ThreadAbort.Net(Net Framework 4.8)

 1 using System;
 2 using System.Threading;
 3 
 4 namespace ExampleCore_6_10.ThreadAbort.Net
 5 {
 6     public class ThreadAbort
 7     {
 8         public void WorkerThread()
 9         {
10             try
11             {
12                 Console.WriteLine("工作者线程执行--工作任务!");
13             }
14             finally
15             {
16                 Console.WriteLine("工作者线程执行--清理工作!");
17             }
18         }
19     }
20 
21     internal class Program
22     {
23         static void Main(string[] args)
24         {
25             ThreadAbort abort = new ThreadAbort();
26             Thread worker = new Thread(abort.WorkerThread);
27 
28             lock (abort)
29             {
30                 worker.Start();
31 
32                 Console.WriteLine("Acquired Lock!");
33                 Thread.Sleep(2000);
34 
35                 Console.WriteLine("Aborting Worker Thread!");
36                 worker.Abort();
37             }
38 
39             Console.Read();
40         }
41     }
42 }
View Code



四、基础知识
    在这一段内容中,有的小节可能会包含两个部分,分别是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母会省略。A 是【基础知识】,讲解必要的知识点,B 是【眼见为实】,通过调试证明讲解的知识点。

    4.1、同步的基础知识
        A、基础知识
            进程:它描述了当一个程序在运行起来所需要的资源总和的统称,包括:CPU、内存、磁盘、网络、GPU 等,最明显我们可以通过【任务管理器】查看我们电脑上运行的进程。
            线程:它是应用程序针对用户操作做出反应的最小执行单元,也就是说,应用软件响应用户的任何操作都是通过一个线程完成的。切记,线程是操作系统的资源,不是 CLR 的,鉴于此,线程具有启动、运行和停止不确定性,也就是启动 N 个线程,每次的启动顺序都可能不一样,同一份代码,同一线程执行的时间也是不同的,启动不同,运行不同,当然,结束的时机也是不同的。
            句柄:是用来标识对象或者项目的标识符,可以用来描述窗体、控件、文件等。
            多线程:能够并发的运行任意数量的线程。

            在这节开始之前,我们必须先弄懂以上 4 个概念,我用自己的语言解释了一下,如果大家不懂,可以自行去网上恶补了。多线程的应用程序如果设计的很好的话,会有三个特征:1、应用程序的用户体验更好,不卡界面;2、应用程序的性能好,处理速度更快;3、多线程具有不确定性,需要我们做更多的工作来协调。

            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 来表示的。

            多线程编程有一个无法避免的问题就是同步的问题,在.NET 中实现同步的方式还是挺多的,比如:事件同步、信号量、互斥体、监视器、瘦锁等。
            在此节,我们会使用【!Process】命令,如果想了解该命令的详细使用方法,可以去微软官网:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/-process

        B、眼见为实
            调试源码:ExampleCore_6_1
            调试任务:我们查看 C# Thread 线程所对应的 OS 层的数据结构表示
            我们直接运行的 EXE 应用程序,程序启动成功,在控制台中输出:tid=4,这个值大家可能不一样。程序运行成功,就产生了一个线程对象。我们想要查看内核态线程的id,需要在借助一个【ProcessExplorer】工具,这个工具有32位和64位两个版本,根据自己系统特特性选择合适的版本,我选择的是64位版本的。
            效果如图:
              

            接着,我们在过【通过名称过滤(Filter by name)】中输入我们项目的名称:ExampleCore_6_1,来进程查找。效果如图:

            

            接着,我们在进程名上双击,打开进程属性对话框,如图:
            

            我们找到了我们项目进程的主线程编号,然后就可以使用 Windbg 查看内核态的线程表示了。我们主线程的编号是:15560,这个是十进制的,要注意。            

            1)、KD 和 NTSD 调试
                说明一下:主线程 ID 不是 15560,我重启了,现在是 2316,效果如图:
                
                我们以管理员身份打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,并输入以下命令:【kd -kl】打开调试器。这个是内核调试器,和【NTSD】是有区别的,【NTSD】是用户态的调试器。
                如图:
                

                打开的调试器窗口如图:
                

                太多了无用内容了,使用【.cls】清理一下。
                执行命令【!process 0 2 ExampleCore_6_1.exe

 1 lkd> !process 0 2 ExampleCore_6_1.exe
 2 PROCESS ffffa2067324d080
 3     SessionId: 1  Cid: 3f2c    Peb: 4e16f21000  ParentCid: 0da8
 4     DirBase: 6bc43002  ObjectTable: 00000000  HandleCount:   0.
 5     Image: ExampleCore_6_1.exe
 6 
 7 No active threads
 8         THREAD ffffa20677bb90c0  Cid 3f2c.3cc8  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
 9         THREAD ffffa20677e50240  Cid 3f2c.3960  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
10         THREAD ffffa20677995080  Cid 3f2c.1f54  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
11         THREAD ffffa2066e255080  Cid 3f2c.3b98  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
12         THREAD ffffa206712dd080  Cid 3f2c.3850  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
13         THREAD ffffa2066ead5080  Cid 3f2c.2144  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
14 
15 PROCESS ffffa206780c8080
16     SessionId: 1  Cid: 4078    Peb: b9e31b9000  ParentCid: 0da8
17     DirBase: 3183bb002  ObjectTable: ffff8a8e17548a00  HandleCount: 171.
18     Image: ExampleCore_6_1.exe
19 
20         THREAD ffffa2066e728080  Cid 4078.090c  Teb: 000000b9e31ba000 Win32Thread: ffffa20677656660 WAIT: (Executive) KernelMode Alertable
21             ffffa20678e5b568  NotificationEvent
22 
23         THREAD ffffa2066e4e1080  Cid 4078.2e48  Teb: 000000b9e31c0000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
24             ffffa20677fe3d60  NotificationEvent
25 
26         THREAD ffffa206757e8080  Cid 4078.336c  Teb: 000000b9e31c2000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
27             ffffa20677fe3c60  SynchronizationEvent
28             ffffa2066f679260  SynchronizationEvent
29             ffffa20677fe39e0  SynchronizationEvent
30 
31         THREAD ffffa206739d4080  Cid 4078.2ef0  Teb: 000000b9e31c4000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
32             ffffa206678be6a0  NotificationEvent
33             ffffa206775ab560  SynchronizationEvent
34 
35         THREAD ffffa20672ea6080  Cid 4078.3750  Teb: 000000b9e31ca000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
36             ffffa20678c15160  SynchronizationEvent

                红色标注就是需要注意的内容,它会把这个进程中的所有线程找出来。我们通过【ProcessExploler】看到我们项目的主线程是:2316,这个值是十进制的,我们看看十六进制是多少。

1 lkd> ?0n2316
2 Evaluate expression: 2316 = 00000000`0000090c

                我再来一个截图显示一下他们的关系,就更清楚了。
                

                ffffa2066e728080 这个值就是线程的内核态的数据结构,我们可以继续使用【dt nt!_KThread ffffa2066e728080】命令查看一下详情。

  1 lkd> dt nt!_KThread ffffa2066e728080
  2    +0x000 Header           : _DISPATCHER_HEADER
  3    +0x018 SListFaultAddress : (null)
  4    +0x020 QuantumTarget    : 0xac9a2b7
  5    +0x028 InitialStack     : 0xffffdf00`c6b27c50 Void
  6    +0x030 StackLimit       : 0xffffdf00`c6b21000 Void
  7    +0x038 StackBase        : 0xffffdf00`c6b28000 Void
  8    +0x040 ThreadLock       : 0
  9    +0x048 CycleTime        : 0x94ce518
 10    +0x050 CurrentRunTime   : 0
 11    +0x054 ExpectedRunTime  : 0x787687
 12    +0x058 KernelStack      : 0xffffdf00`c6b273b0 Void
 13    +0x060 StateSaveArea    : 0xffffdf00`c6b27c80 _XSAVE_FORMAT
 14    +0x068 SchedulingGroup  : (null)
 15    +0x070 WaitRegister     : _KWAIT_STATUS_REGISTER
 16    +0x071 Running          : 0 ''
 17    +0x072 Alerted          : [2]  ""
 18    +0x074 AutoBoostActive  : 0y1
 19    +0x074 ReadyTransition  : 0y0
 20    +0x074 WaitNext         : 0y0
 21    +0x074 SystemAffinityActive : 0y0
 22    +0x074 Alertable        : 0y1
 23    +0x074 UserStackWalkActive : 0y0
 24    +0x074 ApcInterruptRequest : 0y0
 25    +0x074 QuantumEndMigrate : 0y0
 26    +0x074 UmsDirectedSwitchEnable : 0y0
 27    +0x074 TimerActive      : 0y0
 28    +0x074 SystemThread     : 0y0
 29    +0x074 ProcessDetachActive : 0y0
 30    +0x074 CalloutActive    : 0y0
 31    +0x074 ScbReadyQueue    : 0y0
 32    +0x074 ApcQueueable     : 0y1
 33    +0x074 ReservedStackInUse : 0y0
 34    +0x074 UmsPerformingSyscall : 0y0
 35    +0x074 TimerSuspended   : 0y0
 36    +0x074 SuspendedWaitMode : 0y0
 37    +0x074 SuspendSchedulerApcWait : 0y0
 38    +0x074 CetUserShadowStack : 0y0
 39    +0x074 BypassProcessFreeze : 0y0
 40    +0x074 Reserved         : 0y0000000000 (0)
 41    +0x074 MiscFlags        : 0n16401
 42    +0x078 ThreadFlagsSpare : 0y00
 43    +0x078 AutoAlignment    : 0y0
 44    +0x078 DisableBoost     : 0y0
 45    +0x078 AlertedByThreadId : 0y0
 46    +0x078 QuantumDonation  : 0y0
 47    +0x078 EnableStackSwap  : 0y1
 48    +0x078 GuiThread        : 0y1
 49    +0x078 DisableQuantum   : 0y0
 50    +0x078 ChargeOnlySchedulingGroup : 0y0
 51    +0x078 DeferPreemption  : 0y0
 52    +0x078 QueueDeferPreemption : 0y0
 53    +0x078 ForceDeferSchedule : 0y0
 54    +0x078 SharedReadyQueueAffinity : 0y1
 55    +0x078 FreezeCount      : 0y0
 56    +0x078 TerminationApcRequest : 0y0
 57    +0x078 AutoBoostEntriesExhausted : 0y1
 58    +0x078 KernelStackResident : 0y1
 59    +0x078 TerminateRequestReason : 0y00
 60    +0x078 ProcessStackCountDecremented : 0y0
 61    +0x078 RestrictedGuiThread : 0y0
 62    +0x078 VpBackingThread  : 0y0
 63    +0x078 ThreadFlagsSpare2 : 0y0
 64    +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
 65    +0x078 ThreadFlags      : 0n204992
 66    +0x07c Tag              : 0 ''
 67    +0x07d SystemHeteroCpuPolicy : 0 ''
 68    +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
 69    +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
 70    +0x07f RunningNonRetpolineCode : 0y0
 71    +0x07f SpecCtrlSpare    : 0y0000000 (0)
 72    +0x07f SpecCtrl         : 0 ''
 73    +0x080 SystemCallNumber : 6
 74    +0x084 ReadyTime        : 1
 75    +0x088 FirstArgument    : 0x00000000`00000054 Void
 76    +0x090 TrapFrame        : 0xffffdf00`c6b27ac0 _KTRAP_FRAME
 77    +0x098 ApcState         : _KAPC_STATE
 78    +0x098 ApcStateFill     : [43]  "???"
 79    +0x0c3 Priority         : 9 ''
 80    +0x0c4 UserIdealProcessor : 2
 81    +0x0c8 WaitStatus       : 0n0
 82    +0x0d0 WaitBlockList    : 0xffffa206`6e7281c0 _KWAIT_BLOCK
 83    +0x0d8 WaitListEntry    : _LIST_ENTRY [ 0xfffff806`5b7e7aa0 - 0xfffff806`5b7e7aa0 ]
 84    +0x0d8 SwapListEntry    : _SINGLE_LIST_ENTRY
 85    +0x0e8 Queue            : (null)
 86    +0x0f0 Teb              : 0x000000b9`e31ba000 Void
 87    +0x0f8 RelativeTimerBias : 0
 88    +0x100 Timer            : _KTIMER
 89    +0x140 WaitBlock        : [4] _KWAIT_BLOCK
 90    +0x140 WaitBlockFill4   : [20]  "p???"
 91    +0x154 ContextSwitches  : 0xef
 92    +0x140 WaitBlockFill5   : [68]  "p???"
 93    +0x184 State            : 0x5 ''
 94    +0x185 Spare13          : 0 ''
 95    +0x186 WaitIrql         : 0 ''
 96    +0x187 WaitMode         : 0 ''
 97    +0x140 WaitBlockFill6   : [116]  "p???"
 98    +0x1b4 WaitTime         : 0x152d42
 99    +0x140 WaitBlockFill7   : [164]  "p???"
100    +0x1e4 KernelApcDisable : 0n-1
101    +0x1e6 SpecialApcDisable : 0n0
102    +0x1e4 CombinedApcDisable : 0xffff
103    +0x140 WaitBlockFill8   : [40]  "p???"
104    +0x168 ThreadCounters   : (null)
105    +0x140 WaitBlockFill9   : [88]  "p???"
106    +0x198 XStateSave       : (null)
107    +0x140 WaitBlockFill10  : [136]  "p???"
108    +0x1c8 Win32Thread      : 0xffffa206`77656660 Void
109    +0x140 WaitBlockFill11  : [176]  "p???"
110    +0x1f0 Ucb              : (null)
111    +0x1f8 Uch              : (null)
112    +0x200 ThreadFlags2     : 0n0
113    +0x200 BamQosLevel      : 0y00000000 (0)
114    +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
115    +0x204 Spare21          : 0
116    +0x208 QueueListEntry   : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
117    +0x218 NextProcessor    : 1
118    +0x218 NextProcessorNumber : 0y0000000000000000000000000000001 (0x1)
119    +0x218 SharedReadyQueue : 0y0
120    +0x21c QueuePriority    : 0n0
121    +0x220 Process          : 0xffffa206`780c8080 _KPROCESS
122    +0x228 UserAffinity     : _GROUP_AFFINITY
123    +0x228 UserAffinityFill : [10]  "???"
124    +0x232 PreviousMode     : 1 ''
125    +0x233 BasePriority     : 8 ''
126    +0x234 PriorityDecrement : 0 ''
127    +0x234 ForegroundBoost  : 0y0000
128    +0x234 UnusualBoost     : 0y0000
129    +0x235 Preempted        : 0 ''
130    +0x236 AdjustReason     : 0 ''
131    +0x237 AdjustIncrement  : 1 ''
132    +0x238 AffinityVersion  : 0x50
133    +0x240 Affinity         : _GROUP_AFFINITY
134    +0x240 AffinityFill     : [10]  "???"
135    +0x24a ApcStateIndex    : 0 ''
136    +0x24b WaitBlockCount   : 0x1 ''
137    +0x24c IdealProcessor   : 2
138    +0x250 NpxState         : 5
139    +0x258 SavedApcState    : _KAPC_STATE
140    +0x258 SavedApcStateFill : [43]  "???"
141    +0x283 WaitReason       : 0 ''
142    +0x284 SuspendCount     : 0 ''
143    +0x285 Saturation       : 0 ''
144    +0x286 SListFaultCount  : 0
145    +0x288 SchedulerApc     : _KAPC
146    +0x288 SchedulerApcFill1 : [3]  "???"
147    +0x28b QuantumReset     : 0x6 ''
148    +0x288 SchedulerApcFill2 : [4]  "???"
149    +0x28c KernelTime       : 2
150    +0x288 SchedulerApcFill3 : [64]  "???"
151    +0x2c8 WaitPrcb         : (null)
152    +0x288 SchedulerApcFill4 : [72]  "???"
153    +0x2d0 LegoData         : (null)
154    +0x288 SchedulerApcFill5 : [83]  "???"
155    +0x2db CallbackNestingLevel : 0 ''
156    +0x2dc UserTime         : 3
157    +0x2e0 SuspendEvent     : _KEVENT
158    +0x2f8 ThreadListEntry  : _LIST_ENTRY [ 0xffffa206`6e4e1378 - 0xffffa206`780c80b0 ]
159    +0x308 MutantListHead   : _LIST_ENTRY [ 0xffffa206`6e728388 - 0xffffa206`6e728388 ]
160    +0x318 AbEntrySummary   : 0x3e '>'
161    +0x319 AbWaitEntryCount : 0 ''
162    +0x31a AbAllocationRegionCount : 0 ''
163    +0x31b SystemPriority   : 0 ''
164    +0x31c SecureThreadCookie : 0
165    +0x320 LockEntries      : 0xffffa206`6e7286d0 _KLOCK_ENTRY
166    +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
167    +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
168    +0x338 PriorityFloorCounts : [16]  ""
169    +0x348 PriorityFloorCountsReserved : [16]  ""
170    +0x358 PriorityFloorSummary : 0
171    +0x35c AbCompletedIoBoostCount : 0n0
172    +0x360 AbCompletedIoQoSBoostCount : 0n0
173    +0x364 KeReferenceCount : 0n0
174    +0x366 AbOrphanedEntrySummary : 0 ''
175    +0x367 AbOwnedEntryCount : 0x1 ''
176    +0x368 ForegroundLossTime : 0
177    +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
178    +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
179    +0x378 InGlobalForegroundList : 0
180    +0x380 ReadOperationCount : 0n32
181    +0x388 WriteOperationCount : 0n0
182    +0x390 OtherOperationCount : 0n158
183    +0x398 ReadTransferCount : 0n66740
184    +0x3a0 WriteTransferCount : 0n0
185    +0x3a8 OtherTransferCount : 0n3494
186    +0x3b0 QueuedScb        : (null)
187    +0x3b8 ThreadTimerDelay : 0
188    +0x3bc ThreadFlags3     : 0n0
189    +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
190    +0x3bc PpmPolicy        : 0y00
191    +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
192    +0x3c0 TracingPrivate   : [1] 0
193    +0x3c8 SchedulerAssist  : (null)
194    +0x3d0 AbWaitObject     : (null)
195    +0x3d8 ReservedPreviousReadyTimeValue : 0
196    +0x3e0 KernelWaitTime   : 0xe
197    +0x3e8 UserWaitTime     : 0
198    +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
199    +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
200    +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
201    +0x400 SchedulerAssistPriorityFloor : 0n0
202    +0x404 Spare28          : 0
203    +0x408 ResourceIndex    : 0xe7 ''
204    +0x409 Spare31          : [3]  ""
205    +0x410 EndPadding       : [4] 0
206 lkd>
View Code

                当然,我们也可以通过【NTSD -pn ExampleCore_6_1.exe】直接查看正在执行中项目,通过【!t】或者【!threads】命令,查看线程三者的对应关系。

 1 0:005> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      90c 000001CFD8DCEB20    2a020 Preemptive  000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
11    3    2     2ef0 000002106F45DDF0    2b220 Preemptive  0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
12    4    4     3750 000002106F46D070  202b020 Preemptive  000001CFDD40B4D0:000001CFDD40C630 000001CFD8E1B860 -00001 MTA
13 
14 0:005> !threads
15 ThreadCount:      3
16 UnstartedThread:  0
17 BackgroundThread: 1
18 PendingThread:    0
19 DeadThread:       0
20 Hosted Runtime:   no
21                                                                                                             Lock
22  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
23    0    1      90c 000001CFD8DCEB20    2a020 Preemptive  000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
24    3    2     2ef0 000002106F45DDF0    2b220 Preemptive  0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
25    4    4     3750 000002106F46D070  202b020 Preemptive  000001CFDD40B4D0:000001CFDD40C630 000001CFD8E1B860 -00001 MTA
26 0:005>

                ID是 1 就是 C# 的托管线程编号, OSID 的值是 90c 就是操作系统层面的线程的数据结构,ThreadOBJ 就是 CLR 层面的线程。

                
            2)、Windbg Preview 调试
                然后,我们打开 Windbg,点击【File】-->【Attach to kernel(附加内核态)】,在右侧选择【local】,就是本机的内核态,点击【ok】按钮,进入调试界面。然后,我们使用【!process】命令查找一下我们的项目。
 1 lkd> !process 0 2 ExampleCore_6_1.exe
 2 PROCESS ffffa2067324d080
 3     SessionId: 1  Cid: 3f2c    Peb: 4e16f21000  ParentCid: 0da8
 4     DirBase: 6bc43002  ObjectTable: ffff8a8e1a97c180  HandleCount: 171.
 5     Image: ExampleCore_6_1.exe
 6 
 7         THREAD ffffa20677bb90c0  Cid 3f2c.3cc8  Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
 8             ffffa20678223bb8  NotificationEvent
 9 
10         THREAD ffffa20677e50240  Cid 3f2c.3960  Teb: 0000004e16f2a000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
11             ffffa20677fe5660  NotificationEvent
12 
13         THREAD ffffa20677995080  Cid 3f2c.1f54  Teb: 0000004e16f2c000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
14             ffffa20677fe5560  SynchronizationEvent
15             ffffa20677fe56e0  SynchronizationEvent
16             ffffa20677fe5860  SynchronizationEvent
17 
18         THREAD ffffa2066e255080  Cid 3f2c.3b98  Teb: 0000004e16f2e000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
19             ffffa206678be6a0  NotificationEvent
20             ffffa20677cfa260  SynchronizationEvent
21 
22         THREAD ffffa206712dd080  Cid 3f2c.3850  Teb: 0000004e16f34000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
23             ffffa20677964c60  SynchronizationEvent

                我们通过【ProcessExploler】看到我们项目的主线程是:1204,这个值是十进制的,我们看看十六进制是多少。

1 lkd> ? 0n15560
2 Evaluate expression: 15560 = 00000000`00003cc8

                我们如果使用的调试器是【Windbg Preview】,它有一个特性,选择一个文本,和文本内容相同的也会被凸显出来,我们选择 3cc8,发现我们使用【!process】命令的结果中也有被选择了,如图:
                

                ffffa20677bb90c0 这个值就是线程的内核态的数据结构,我们可以继续使用【dt】命令查看一下详情。

  1 lkd> dt nt!_KThread ffffa20677bb90c0
  2    +0x000 Header           : _DISPATCHER_HEADER
  3    +0x018 SListFaultAddress : (null) 
  4    +0x020 QuantumTarget    : 0xd630923
  5    +0x028 InitialStack     : 0xffffdf00`c2f32c50 Void
  6    +0x030 StackLimit       : 0xffffdf00`c2f2c000 Void
  7    +0x038 StackBase        : 0xffffdf00`c2f33000 Void
  8    +0x040 ThreadLock       : 0
  9    +0x048 CycleTime        : 0x94e88f3
 10    +0x050 CurrentRunTime   : 0
 11    +0x054 ExpectedRunTime  : 0xa80710
 12    +0x058 KernelStack      : 0xffffdf00`c2f323b0 Void
 13    +0x060 StateSaveArea    : 0xffffdf00`c2f32c80 _XSAVE_FORMAT
 14    +0x068 SchedulingGroup  : (null) 
 15    +0x070 WaitRegister     : _KWAIT_STATUS_REGISTER
 16    +0x071 Running          : 0 ''
 17    +0x072 Alerted          : [2]  ""
 18    +0x074 AutoBoostActive  : 0y1
 19    +0x074 ReadyTransition  : 0y0
 20    +0x074 WaitNext         : 0y0
 21    +0x074 SystemAffinityActive : 0y0
 22    +0x074 Alertable        : 0y1
 23    +0x074 UserStackWalkActive : 0y0
 24    +0x074 ApcInterruptRequest : 0y0
 25    +0x074 QuantumEndMigrate : 0y0
 26    +0x074 UmsDirectedSwitchEnable : 0y0
 27    +0x074 TimerActive      : 0y0
 28    +0x074 SystemThread     : 0y0
 29    +0x074 ProcessDetachActive : 0y0
 30    +0x074 CalloutActive    : 0y0
 31    +0x074 ScbReadyQueue    : 0y0
 32    +0x074 ApcQueueable     : 0y1
 33    +0x074 ReservedStackInUse : 0y0
 34    +0x074 UmsPerformingSyscall : 0y0
 35    +0x074 TimerSuspended   : 0y0
 36    +0x074 SuspendedWaitMode : 0y0
 37    +0x074 SuspendSchedulerApcWait : 0y0
 38    +0x074 CetUserShadowStack : 0y0
 39    +0x074 BypassProcessFreeze : 0y0
 40    +0x074 Reserved         : 0y0000000000 (0)
 41    +0x074 MiscFlags        : 0n16401
 42    +0x078 ThreadFlagsSpare : 0y00
 43    +0x078 AutoAlignment    : 0y0
 44    +0x078 DisableBoost     : 0y0
 45    +0x078 AlertedByThreadId : 0y0
 46    +0x078 QuantumDonation  : 0y0
 47    +0x078 EnableStackSwap  : 0y1
 48    +0x078 GuiThread        : 0y1
 49    +0x078 DisableQuantum   : 0y0
 50    +0x078 ChargeOnlySchedulingGroup : 0y0
 51    +0x078 DeferPreemption  : 0y0
 52    +0x078 QueueDeferPreemption : 0y0
 53    +0x078 ForceDeferSchedule : 0y0
 54    +0x078 SharedReadyQueueAffinity : 0y1
 55    +0x078 FreezeCount      : 0y0
 56    +0x078 TerminationApcRequest : 0y0
 57    +0x078 AutoBoostEntriesExhausted : 0y1
 58    +0x078 KernelStackResident : 0y1
 59    +0x078 TerminateRequestReason : 0y00
 60    +0x078 ProcessStackCountDecremented : 0y0
 61    +0x078 RestrictedGuiThread : 0y0
 62    +0x078 VpBackingThread  : 0y0
 63    +0x078 ThreadFlagsSpare2 : 0y0
 64    +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
 65    +0x078 ThreadFlags      : 0n204992
 66    +0x07c Tag              : 0 ''
 67    +0x07d SystemHeteroCpuPolicy : 0 ''
 68    +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
 69    +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
 70    +0x07f RunningNonRetpolineCode : 0y0
 71    +0x07f SpecCtrlSpare    : 0y0000000 (0)
 72    +0x07f SpecCtrl         : 0 ''
 73    +0x080 SystemCallNumber : 6
 74    +0x084 ReadyTime        : 3
 75    +0x088 FirstArgument    : 0x00000000`00000050 Void
 76    +0x090 TrapFrame        : 0xffffdf00`c2f32ac0 _KTRAP_FRAME
 77    +0x098 ApcState         : _KAPC_STATE
 78    +0x098 ApcStateFill     : [43]  "X???"
 79    +0x0c3 Priority         : 8 ''
 80    +0x0c4 UserIdealProcessor : 2
 81    +0x0c8 WaitStatus       : 0n256
 82    +0x0d0 WaitBlockList    : 0xffffa206`77bb9200 _KWAIT_BLOCK
 83    +0x0d8 WaitListEntry    : _LIST_ENTRY [ 0x00000000`00000000 - 0xffffa206`67903158 ]
 84    +0x0d8 SwapListEntry    : _SINGLE_LIST_ENTRY
 85    +0x0e8 Queue            : (null) 
 86    +0x0f0 Teb              : 0x0000004e`16f22000 Void
 87    +0x0f8 RelativeTimerBias : 0
 88    +0x100 Timer            : _KTIMER
 89    +0x140 WaitBlock        : [4] _KWAIT_BLOCK
 90    +0x140 WaitBlockFill4   : [20]  "???"
 91    +0x154 ContextSwitches  : 0xde
 92    +0x140 WaitBlockFill5   : [68]  "???"
 93    +0x184 State            : 0x5 ''
 94    +0x185 Spare13          : 0 ''
 95    +0x186 WaitIrql         : 0 ''
 96    +0x187 WaitMode         : 0 ''
 97    +0x140 WaitBlockFill6   : [116]  "???"
 98    +0x1b4 WaitTime         : 0x11f7e8
 99    +0x140 WaitBlockFill7   : [164]  "???"
100    +0x1e4 KernelApcDisable : 0n-1
101    +0x1e6 SpecialApcDisable : 0n0
102    +0x1e4 CombinedApcDisable : 0xffff
103    +0x140 WaitBlockFill8   : [40]  "???"
104    +0x168 ThreadCounters   : (null) 
105    +0x140 WaitBlockFill9   : [88]  "???"
106    +0x198 XStateSave       : (null) 
107    +0x140 WaitBlockFill10  : [136]  "???"
108    +0x1c8 Win32Thread      : 0xffffa206`7765a990 Void
109    +0x140 WaitBlockFill11  : [176]  "???"
110    +0x1f0 Ucb              : (null) 
111    +0x1f8 Uch              : (null) 
112    +0x200 ThreadFlags2     : 0n0
113    +0x200 BamQosLevel      : 0y00000000 (0)
114    +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
115    +0x204 Spare21          : 0
116    +0x208 QueueListEntry   : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
117    +0x218 NextProcessor    : 2
118    +0x218 NextProcessorNumber : 0y0000000000000000000000000000010 (0x2)
119    +0x218 SharedReadyQueue : 0y0
120    +0x21c QueuePriority    : 0n0
121    +0x220 Process          : 0xffffa206`7324d080 _KPROCESS
122    +0x228 UserAffinity     : _GROUP_AFFINITY
123    +0x228 UserAffinityFill : [10]  "???"
124    +0x232 PreviousMode     : 1 ''
125    +0x233 BasePriority     : 8 ''
126    +0x234 PriorityDecrement : 0 ''
127    +0x234 ForegroundBoost  : 0y0000
128    +0x234 UnusualBoost     : 0y0000
129    +0x235 Preempted        : 0 ''
130    +0x236 AdjustReason     : 0 ''
131    +0x237 AdjustIncrement  : 0 ''
132    +0x238 AffinityVersion  : 0x50
133    +0x240 Affinity         : _GROUP_AFFINITY
134    +0x240 AffinityFill     : [10]  "???"
135    +0x24a ApcStateIndex    : 0 ''
136    +0x24b WaitBlockCount   : 0x1 ''
137    +0x24c IdealProcessor   : 2
138    +0x250 NpxState         : 5
139    +0x258 SavedApcState    : _KAPC_STATE
140    +0x258 SavedApcStateFill : [43]  "???"
141    +0x283 WaitReason       : 0 ''
142    +0x284 SuspendCount     : 0 ''
143    +0x285 Saturation       : 0 ''
144    +0x286 SListFaultCount  : 0
145    +0x288 SchedulerApc     : _KAPC
146    +0x288 SchedulerApcFill1 : [3]  "???"
147    +0x28b QuantumReset     : 0x6 ''
148    +0x288 SchedulerApcFill2 : [4]  "???"
149    +0x28c KernelTime       : 1
150    +0x288 SchedulerApcFill3 : [64]  "???"
151    +0x2c8 WaitPrcb         : (null) 
152    +0x288 SchedulerApcFill4 : [72]  "???"
153    +0x2d0 LegoData         : (null) 
154    +0x288 SchedulerApcFill5 : [83]  "???"
155    +0x2db CallbackNestingLevel : 0 ''
156    +0x2dc UserTime         : 2
157    +0x2e0 SuspendEvent     : _KEVENT
158    +0x2f8 ThreadListEntry  : _LIST_ENTRY [ 0xffffa206`77e50538 - 0xffffa206`7324d0b0 ]
159    +0x308 MutantListHead   : _LIST_ENTRY [ 0xffffa206`77bb93c8 - 0xffffa206`77bb93c8 ]
160    +0x318 AbEntrySummary   : 0x3e '>'
161    +0x319 AbWaitEntryCount : 0 ''
162    +0x31a AbAllocationRegionCount : 0 ''
163    +0x31b SystemPriority   : 0 ''
164    +0x31c SecureThreadCookie : 0
165    +0x320 LockEntries      : 0xffffa206`77bb9710 _KLOCK_ENTRY
166    +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
167    +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
168    +0x338 PriorityFloorCounts : [16]  ""
169    +0x348 PriorityFloorCountsReserved : [16]  ""
170    +0x358 PriorityFloorSummary : 0
171    +0x35c AbCompletedIoBoostCount : 0n0
172    +0x360 AbCompletedIoQoSBoostCount : 0n0
173    +0x364 KeReferenceCount : 0n0
174    +0x366 AbOrphanedEntrySummary : 0 ''
175    +0x367 AbOwnedEntryCount : 0x1 ''
176    +0x368 ForegroundLossTime : 0
177    +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
178    +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
179    +0x378 InGlobalForegroundList : 0
180    +0x380 ReadOperationCount : 0n32
181    +0x388 WriteOperationCount : 0n0
182    +0x390 OtherOperationCount : 0n158
183    +0x398 ReadTransferCount : 0n66740
184    +0x3a0 WriteTransferCount : 0n0
185    +0x3a8 OtherTransferCount : 0n3494
186    +0x3b0 QueuedScb        : (null) 
187    +0x3b8 ThreadTimerDelay : 0
188    +0x3bc ThreadFlags3     : 0n0
189    +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
190    +0x3bc PpmPolicy        : 0y00
191    +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
192    +0x3c0 TracingPrivate   : [1] 0
193    +0x3c8 SchedulerAssist  : (null) 
194    +0x3d0 AbWaitObject     : (null) 
195    +0x3d8 ReservedPreviousReadyTimeValue : 0
196    +0x3e0 KernelWaitTime   : 0xe
197    +0x3e8 UserWaitTime     : 0
198    +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
199    +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
200    +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
201    +0x400 SchedulerAssistPriorityFloor : 0n0
202    +0x404 Spare28          : 0
203    +0x408 ResourceIndex    : 0x1 ''
204    +0x409 Spare31          : [3]  ""
205    +0x410 EndPadding       : [4] 0
View Code

                这个线程的数据结构内容还是不少的。
                我们可以使用【!thread ffffa20677bb90c0】命令查看更易阅读的结果。

 1 lkd> !thread ffffa20677bb90c0
 2 THREAD ffffa20677bb90c0  Cid 3f2c.3cc8  Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
 3     ffffa20678223bb8  NotificationEvent
 4 IRP List:
 5     ffffa2067802cdc0: (0006,0160) Flags: 00060900  Mdl: ffffa20670216220
 6     ffffa2067802bc80: (0006,0160) Flags: 00060800  Mdl: 00000000
 7 Not impersonating
 8 DeviceMap                 ffff8a8e0d39f7e0
 9 Owning Process            ffffa2067324d080       Image:         ExampleCore_6_1.exe
10 Attached Process          N/A            Image:         N/A
11 Wait Start TickCount      1177576        Ticks: 163639 (0:00:42:36.859)
12 Context Switch Count      222            IdealProcessor: 2             
13 UserTime                  00:00:00.031
14 KernelTime                00:00:00.015
15 Win32 Start Address 0x00007ff7359f1360
16 Stack Init ffffdf00c2f32c50 Current ffffdf00c2f323b0
17 Base ffffdf00c2f33000 Limit ffffdf00c2f2c000 Call 0000000000000000
18 Priority 8  BasePriority 8  IoPriority 2  PagePriority 5
19 Child-SP          RetAddr               : Args to Child                                                           : Call Site
20 ffffdf00`c2f323f0 fffff806`5d841330     : ffffbb80`50317180 00000000`ffffffff ffffa206`00000000 00000000`50317180 : nt!KiSwapContext+0x76
21 ffffdf00`c2f32530 fffff806`5d84085f     : 00000000`00000002 ffff8a8e`00000000 ffffdf00`c2f326f0 fffff806`00000000 : nt!KiSwapThread+0x500
22 ffffdf00`c2f325e0 fffff806`5d840103     : 000002af`00000000 00000000`00000000 00000000`00000000 ffffa206`77bb9200 : nt!KiCommitThreadWait+0x14f
23 ffffdf00`c2f32680 fffff806`5d9f18bc     : ffffa206`78223bb8 ffffa206`00000000 00000000`00000000 ffffa206`77bb9001 : nt!KeWaitForSingleObject+0x233
24 ffffdf00`c2f32770 fffff806`5dc45b5b     : 00000000`00000000 00000000`00000001 ffffa206`78223b20 ffffa206`7802cdc0 : nt!IopWaitForSynchronousIoEvent+0x50
25 ffffdf00`c2f327b0 fffff806`5dbcf918     : ffffdf00`c2f32b40 ffffa206`78223b20 00000000`00000000 00000000`00000000 : nt!IopSynchronousServiceTail+0x50b
26 ffffdf00`c2f32850 fffff806`5dc0c4b8     : ffffa206`78223b20 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopReadFile+0x7cc
27 ffffdf00`c2f32940 fffff806`5da11578     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtReadFile+0x8a8
28 ffffdf00`c2f32a50 00007ffa`7f08d0a4     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x28 (TrapFrame @ ffffdf00`c2f32ac0)
29 0000004e`1717e558 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0x00007ffa`7f08d0a4

                当然,我们也可以通过 Windbg Preview 直接查看了,我们的项目正在执行中,所以我们可以通过【Attach to process】进入调试界面,然后,通过【!t】或者【!threads】命令,查看线程三者的对应关系。

 1 0:005> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     3cc8 00000246EFD07630    2a020 Preemptive  00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA 
11    3    2     3b98 00000246EFD70060    2b220 Preemptive  0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer) 
12    4    4     3850 00000246EFCCD3F0  202b020 Preemptive  00000246F440B4D0:00000246F440C630 00000246efca59d0 -00001 MTA 
13 
14 0:005> !threads
15 ThreadCount:      3
16 UnstartedThread:  0
17 BackgroundThread: 1
18 PendingThread:    0
19 DeadThread:       0
20 Hosted Runtime:   no
21                                                                                                             Lock  
22  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
23    0    1     3cc8 00000246EFD07630    2a020 Preemptive  00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA 
24    3    2     3b98 00000246EFD70060    2b220 Preemptive  0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer) 
25    4    4     3850 00000246EFCCD3F0  202b020 Preemptive  00000246F440B4D0:00000246F440C630 00000246efca59d0 -00001 MTA 

                我们在【!t/threads】命令的结果中,查看【OSID】列,也能看到 3cc8 的标识。ID是1就是C#的托管线程编号, OSID的值是 3cc8 就是操作系统层面的线程的数据结构,ThreadOBJ 就是 CLR 层面的线程。


    4.2、线程同步原语
        在开始之前,先解释一下以下概念:用户态和内核态,这两个概念不清楚,就会搞得云里雾里的。
        用户态:
          用户态也被称为用户模式,是指应用程序的运行状态。在这种模式下,应用程序拥有有限的系统资源访问权限,只能在操作系统划定的特定空间内运行。用户态下运行的程序不能直接访问硬件设备或执行特权指令,所有对硬件的访问都必须通过操作系统进行。
          在用户态下,应用程序通过系统调用来请求操作系统提供的服务。例如,文件操作、网络通信等都需要通过系统调用来实现。当应用程序发出系统调用时,会触发上下文切换,将CPU的控制权交给操作系统内核,进入内核态。

        内核态:
          内核态也被称为内核模式或特权模式,是操作系统内核的运行状态。处于内核态的CPU可以执行所有的指令,访问所有的内存地址,拥有最高的权限。内核态下运行的程序可以访问系统的所有资源,包括CPU、内存、I/O等。
          在内核态下,操作系统可以响应所有的中断请求,处理硬件事件和系统调用。当应用程序发出系统调用时,CPU会切换到内核态,执行相应的操作,然后返回用户态。此外,当发生严重错误或异常时,也会触发内核态的切换。

        Windows 操作系统在内部使用线程执行块(Thread Execution Block,TEB)这种数据结构来表示一个线程。该数据结构包含了线程的各种属性,例如线程标识、最近发生的错误、局部存储等,我们可以执行【dt _TEB】命令查看这个数据结构。
        我在调试器中加载 ExampleCore_6_1.exe 项目,执行【dt _TEB】命令,输出如下:

  1 0:010> dt _TEB
  2 ntdll!_TEB
  3    +0x000 NtTib            : _NT_TIB
  4    +0x038 EnvironmentPointer : Ptr64 Void
  5    +0x040 ClientId         : _CLIENT_ID
  6    +0x050 ActiveRpcHandle  : Ptr64 Void
  7    +0x058 ThreadLocalStoragePointer : Ptr64 Void
  8    +0x060 ProcessEnvironmentBlock : Ptr64 _PEB
  9    +0x068 LastErrorValue   : Uint4B
 10    +0x06c CountOfOwnedCriticalSections : Uint4B
 11    +0x070 CsrClientThread  : Ptr64 Void
 12    +0x078 Win32ThreadInfo  : Ptr64 Void
 13    +0x080 User32Reserved   : [26] Uint4B
 14    +0x0e8 UserReserved     : [5] Uint4B
 15    +0x100 WOW32Reserved    : Ptr64 Void
 16    +0x108 CurrentLocale    : Uint4B
 17    +0x10c FpSoftwareStatusRegister : Uint4B
 18    +0x110 ReservedForDebuggerInstrumentation : [16] Ptr64 Void
 19    +0x190 SystemReserved1  : [30] Ptr64 Void
 20    +0x280 PlaceholderCompatibilityMode : Char
 21    +0x281 PlaceholderHydrationAlwaysExplicit : UChar
 22    +0x282 PlaceholderReserved : [10] Char
 23    +0x28c ProxiedProcessId : Uint4B
 24    +0x290 _ActivationStack : _ACTIVATION_CONTEXT_STACK
 25    +0x2b8 WorkingOnBehalfTicket : [8] UChar
 26    +0x2c0 ExceptionCode    : Int4B
 27    +0x2c4 Padding0         : [4] UChar
 28    +0x2c8 ActivationContextStackPointer : Ptr64 _ACTIVATION_CONTEXT_STACK
 29    +0x2d0 InstrumentationCallbackSp : Uint8B
 30    +0x2d8 InstrumentationCallbackPreviousPc : Uint8B
 31    +0x2e0 InstrumentationCallbackPreviousSp : Uint8B
 32    +0x2e8 TxFsContext      : Uint4B
 33    +0x2ec InstrumentationCallbackDisabled : UChar
 34    +0x2ed UnalignedLoadStoreExceptions : UChar
 35    +0x2ee Padding1         : [2] UChar
 36    +0x2f0 GdiTebBatch      : _GDI_TEB_BATCH
 37    +0x7d8 RealClientId     : _CLIENT_ID
 38    +0x7e8 GdiCachedProcessHandle : Ptr64 Void
 39    +0x7f0 GdiClientPID     : Uint4B
 40    +0x7f4 GdiClientTID     : Uint4B
 41    +0x7f8 GdiThreadLocalInfo : Ptr64 Void
 42    +0x800 Win32ClientInfo  : [62] Uint8B
 43    +0x9f0 glDispatchTable  : [233] Ptr64 Void
 44    +0x1138 glReserved1      : [29] Uint8B
 45    +0x1220 glReserved2      : Ptr64 Void
 46    +0x1228 glSectionInfo    : Ptr64 Void
 47    +0x1230 glSection        : Ptr64 Void
 48    +0x1238 glTable          : Ptr64 Void
 49    +0x1240 glCurrentRC      : Ptr64 Void
 50    +0x1248 glContext        : Ptr64 Void
 51    +0x1250 LastStatusValue  : Uint4B
 52    +0x1254 Padding2         : [4] UChar
 53    +0x1258 StaticUnicodeString : _UNICODE_STRING
 54    +0x1268 StaticUnicodeBuffer : [261] Wchar
 55    +0x1472 Padding3         : [6] UChar
 56    +0x1478 DeallocationStack : Ptr64 Void
 57    +0x1480 TlsSlots         : [64] Ptr64 Void
 58    +0x1680 TlsLinks         : _LIST_ENTRY
 59    +0x1690 Vdm              : Ptr64 Void
 60    +0x1698 ReservedForNtRpc : Ptr64 Void
 61    +0x16a0 DbgSsReserved    : [2] Ptr64 Void
 62    +0x16b0 HardErrorMode    : Uint4B
 63    +0x16b4 Padding4         : [4] UChar
 64    +0x16b8 Instrumentation  : [11] Ptr64 Void
 65    +0x1710 ActivityId       : _GUID
 66    +0x1720 SubProcessTag    : Ptr64 Void
 67    +0x1728 PerflibData      : Ptr64 Void
 68    +0x1730 EtwTraceData     : Ptr64 Void
 69    +0x1738 WinSockData      : Ptr64 Void
 70    +0x1740 GdiBatchCount    : Uint4B
 71    +0x1744 CurrentIdealProcessor : _PROCESSOR_NUMBER
 72    +0x1744 IdealProcessorValue : Uint4B
 73    +0x1744 ReservedPad0     : UChar
 74    +0x1745 ReservedPad1     : UChar
 75    +0x1746 ReservedPad2     : UChar
 76    +0x1747 IdealProcessor   : UChar
 77    +0x1748 GuaranteedStackBytes : Uint4B
 78    +0x174c Padding5         : [4] UChar
 79    +0x1750 ReservedForPerf  : Ptr64 Void
 80    +0x1758 ReservedForOle   : Ptr64 Void
 81    +0x1760 WaitingOnLoaderLock : Uint4B
 82    +0x1764 Padding6         : [4] UChar
 83    +0x1768 SavedPriorityState : Ptr64 Void
 84    +0x1770 ReservedForCodeCoverage : Uint8B
 85    +0x1778 ThreadPoolData   : Ptr64 Void
 86    +0x1780 TlsExpansionSlots : Ptr64 Ptr64 Void
 87    +0x1788 DeallocationBStore : Ptr64 Void
 88    +0x1790 BStoreLimit      : Ptr64 Void
 89    +0x1798 MuiGeneration    : Uint4B
 90    +0x179c IsImpersonating  : Uint4B
 91    +0x17a0 NlsCache         : Ptr64 Void
 92    +0x17a8 pShimData        : Ptr64 Void
 93    +0x17b0 HeapData         : Uint4B
 94    +0x17b4 Padding7         : [4] UChar
 95    +0x17b8 CurrentTransactionHandle : Ptr64 Void
 96    +0x17c0 ActiveFrame      : Ptr64 _TEB_ACTIVE_FRAME
 97    +0x17c8 FlsData          : Ptr64 Void
 98    +0x17d0 PreferredLanguages : Ptr64 Void
 99    +0x17d8 UserPrefLanguages : Ptr64 Void
100    +0x17e0 MergedPrefLanguages : Ptr64 Void
101    +0x17e8 MuiImpersonation : Uint4B
102    +0x17ec CrossTebFlags    : Uint2B
103    +0x17ec SpareCrossTebBits : Pos 0, 16 Bits
104    +0x17ee SameTebFlags     : Uint2B
105    +0x17ee SafeThunkCall    : Pos 0, 1 Bit
106    +0x17ee InDebugPrint     : Pos 1, 1 Bit
107    +0x17ee HasFiberData     : Pos 2, 1 Bit
108    +0x17ee SkipThreadAttach : Pos 3, 1 Bit
109    +0x17ee WerInShipAssertCode : Pos 4, 1 Bit
110    +0x17ee RanProcessInit   : Pos 5, 1 Bit
111    +0x17ee ClonedThread     : Pos 6, 1 Bit
112    +0x17ee SuppressDebugMsg : Pos 7, 1 Bit
113    +0x17ee DisableUserStackWalk : Pos 8, 1 Bit
114    +0x17ee RtlExceptionAttached : Pos 9, 1 Bit
115    +0x17ee InitialThread    : Pos 10, 1 Bit
116    +0x17ee SessionAware     : Pos 11, 1 Bit
117    +0x17ee LoadOwner        : Pos 12, 1 Bit
118    +0x17ee LoaderWorker     : Pos 13, 1 Bit
119    +0x17ee SkipLoaderInit   : Pos 14, 1 Bit
120    +0x17ee SpareSameTebBits : Pos 15, 1 Bit
121    +0x17f0 TxnScopeEnterCallback : Ptr64 Void
122    +0x17f8 TxnScopeExitCallback : Ptr64 Void
123    +0x1800 TxnScopeContext  : Ptr64 Void
124    +0x1808 LockCount        : Uint4B
125    +0x180c WowTebOffset     : Int4B
126    +0x1810 ResourceRetValue : Ptr64 Void
127    +0x1818 ReservedForWdf   : Ptr64 Void
128    +0x1820 ReservedForCrt   : Uint8B
129    +0x1828 EffectiveContainerId : _GUID
130 0:010>



        4.2.1、事件同步原语(AutoResetEvent 和 ManulResetEvent(内核锁))
            A、基础知识
                事件同步的本质是在内核态维护了一个 bool 值,通过 bool 值来实现线程间的同步,具体的使用方法网上很多,我这里就不过多的赘述了,这里我们看看是如何通过 bool 值的变化实现线程间的同步的。
                事件是一种内核态的原语,可以在用户态中通过句柄来访问。事件也是一个同步对象,它有两种状态:已触发(signaled)和未触发(nonsignaled)。当事件是未触发的状态,在这个事件上的线程就会处于等待的状态,如果事件的状态变为已触发时,这个线程也会恢复执行。
                事件对象经常用于对多个线程之间的代码执行流程进行同步。
                
AutoResetEvent 和 ManulResetEvent 区别:ManulResetEvent 在手动重置事件中,事件对象保持为已触发的状态,直到被手动重置,因此,所有在这个事件对象上等待的线程都会被释放。AutoResetEvent 自动重置事件只允许其中一个等待线程被释放,然后,又立即自动的回到未触发状态。如果没有任何等待的线程,那么这个事件对象将保持为未触发的状态,直到第一个线程在这个事件上开始等待。

               我们都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底层的功能,说白了就是 C# 只是使用了 Windows 内核提供的事件,C# 不过是对其进行了包装,如果你想要查看内存地址,必须到内核态去看。

               AutoResetEvent 或者 ManulResetEvent 类型内部包含了 SafeWaitHandle 引用类型的一个字段 _waitHandle,_waitHandle 类型内部包含了一个值类型的(System.IntPtr)的 handle 实现的同步操作。

            B、眼见为实
                调试源码:ExampleCore_6_2
                调试任务:我们看看 AutoResetEvent 是如何通过 bool 值变化实现线程间的同步的。
                注意:这里的调试都需要用到两种调试器,分别是用户态的和内核态的,还有一个获取对象内核地址的工具【Process Explorer】。在用户态调试器执行调用,在内核态调试器里看具体地址内容的变化。
                1)、KD 和 NTSD 调试
                    在这里,我只测试 ManualResetEvent 类型的变化,AutoResetEvent 暂时我忽略,因为它们没区别。调试器使用用户态的 NTSD 和内核态的 KD。
                    编译我们的项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_2\bin\Debug\net8.0\ExampleCore_6_2.exe】打开调试器。
                    进入调试器后,【g】直接运行,直到调试器输出“选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)”字样,我们输入 manual,不区分大小写,就进入到了 RunManualResetEvent 方法内,调试器会输出“mre 默认为 false,即等待状态,请查看!”字样。调试器中断执行,开始我们的调试了。
                    首先,我们在托管堆上查找 ManualResetEvent 类型的对象,执行命令【!DumpHeap -type ManualResetEvent】。

1 0:000> !DumpHeap -type ManualResetEvent
2          Address               MT     Size
3 0000020f29414180 00007ff8db192a88       24
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ff8db192a88        1           24 System.Threading.ManualResetEvent
8 Total 1 objects

                    ManualResetEvent 对象的地址是 0000020f29414180,我们继续使用【!do】或者【!DumpObj】命令查看它的详情。

 1 0:000> !do 0000020f29414180
 2 Name:        System.Threading.ManualResetEvent
 3 MethodTable: 00007ff8db192a88
 4 EEClass:     00007ff8db182508
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8db193318  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000020f294142d8 _waitHandle
11 00007ff8db0370a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    红色标注的就是一个引用类型实例,地址是 0000020f294142d8,针对该地址,继续执行【!do】命令。

 1 0:000> !do 0000020f294142d8
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ff8db193318
 4 EEClass:     00007ff8db182970
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8db0370a0  400126e        8        System.IntPtr  1 instance 00000000000002B8 handle
11 00007ff8dafc1188  400126f       10         System.Int32  1 instance                4 _state
12 00007ff8daf8d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ff8daf8d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    红色标注的是一个 handle 对象,我们可以使用【!handle 00000000000002B8 f】命令继续查看,必须具有 f 参数。

 1 0:000> !handle 00000000000002B8 f
 2 Handle 2b8
 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(初始状态是等待)

                    到此,说明 ManualResetEvent(false) 默认是等待的状态。
                    此刻,我们在借助【Process Explorer】工具,找到事件同步对象的内核地址,看看内核地址上的数据的变化。打开这个工具,然后在【Filter by name】输入项目名称 ExampleCore_6_2,结果如图:

                    

                    我们在【Handles】选项里,找到我们的事件对象,然后双击,打开属性框,找到内核的地址。如图:
                    

                    我们找到了事件对象在内核上的地址,我们需要再打开一个【kd】调试器,开始内核调试。
                    我们就找到了内核地址【0xFFFF940C4DC558E0】了。然后,我们到 kd 的内核态中去查看一下这个地址,使用【dp 0xFFFF940C4DC558E0 l1】命令。当前值:0(00000000)

 

1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0  00000000`00060000

                    说明 ManualResetEvent 的 fase 表示的是等待,通过用户态命令【!handle 00000000000002B8 f】和内核态命令【dp 0xFFFF940C4DC558E0 l1】都能证明。
                    然后我们【g】一下用户态的 NTSD 调试器,控制台输出“mre 默认为 true,即放行状态,请查看!”字样,再次执行命令【!handle 00000000000002B8 f】。

 1 0:000> !handle 00000000000002B8 f
 2 Handle 2b8
 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 Set(放行状态)

                    然后切换到【内核态】的 KD 调试器,继续使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。

1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0  00000001`00060000(红色变成 1 ,表示 true)

                    【!handle】命令的结果是 Set,【dp】命令变成了 00000001,后面的不用管。
                    最后,我们再【g】一下【用户态】的 KD,控制台输出“mre Reset后为 false,即等待状态,请查看!”字样,再次执行【!handle 00000000000002B8 f】命令。

 1 0:000> !handle 00000000000002B8 f
 2 Handle 2b8
 3   Type          Event
 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     Event Type Manual Reset
13     Event is Waiting(处于等待)

                    Reset 后是等待的状态,然后切换到【内核态】的 KD,继续使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。

1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0  00000000`00060000(红色是 0,0 代表就是 false)

                    我们就看到了,状态是0和1相互切换的。


                2)、Windbg Preview 调试
                    我们编译项目,打开【Windbg Preview】调试器,点击【文件】----》【Launch executable】加载我们的程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,在【Debugger.Break()】语句处停止,我们的控制台应用程序输出:mre 默认为 false,即等待状态,请查看!,Windbg 处于暂停状态,我们就可以调试了。
                    首先,我们去托管堆中查找一下 ManualResetEvent 这个对象,执行【!dumpheap -type ManualResetEvent】命令。
1 0:000> !DumpHeap -type ManualResetEvent
2          Address               MT           Size
3     012b87014180     7ff8da3e2a88             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ff8da3e2a88     1        24 System.Threading.ManualResetEvent
8 Total 1 objects, 24 bytes
                    ManualResetEvent 对象的地址是 012b87014180,针对这个地址,我们使用【!do】或者【!DumpObj】命令,查看它的详情。
 1 0:000> !DumpObj 012b87014180
 2 Name:        System.Threading.ManualResetEvent(手动重置事件)
 3 MethodTable: 00007ff8da3e2a88
 4 EEClass:     00007ff8da3d2508
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da3e3318  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000012b870142d8 _waitHandle
11 00007ff8da2870a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<
                    红色标注的是一个 instance 引用类型(VT=0)实例对象,我们可以使用【!DumpObj 0000012b870142d8】命令继续查看。
 1 0:000> !DumpObj 0000012b870142d8
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ff8da3e3318
 4 EEClass:     00007ff8da3d2968
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da2870a0  400126e        8        System.IntPtr  1 instance 0000000000000248 handle
11 00007ff8da211188  400126f       10         System.Int32  1 instance                4 _state
12 00007ff8da1dd070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ff8da1dd070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    红色标注的是一个 System.IntPtr 值类型(VT=1)实例对象,我们可以使用【!DumpVC 00007ff8da2870a0  0000000000000248】命令继续查看。

 1 0:000> !DumpVC 00007ff8da2870a0  0000000000000248
 2 Name:        System.IntPtr
 3 MethodTable: 00007ff8da2870a0
 4 EEClass:     00007ff8da266100
 5 Size:        24(0x18) bytes
 6 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 7 Fields:
 8               MT    Field   Offset                 Type VT     Attr            Value Name
 9 00007ff8da2870a0  4000525        0        System.IntPtr  1 instance  _value
10 00007ff8da2870a0  4000526      a78        System.IntPtr  1   static 0000000000000000 Zero
                    我们可以不使用【!DumpVC】命令,直接使用【!handle】命令。
                    红色标注的是一个 handle 对象,我们可以使用【!handle 0000000000000248 f】命令继续查看,必须具有 f 参数。
 1 0:000> !handle 0000000000000248 f
 2 Handle 248
 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(事件类型是 ManualResetEvent13     Event is Waiting(当前是等待状态

                    说明 false 是等待的状态,然后,我们继续【g】运行一下,等我们的控制台项目输出:mre 默认为 true,即放行状态,请查看!,我们继续执行【!handle 0000000000000248 f】命令查看。

 1 0:000> !handle 0000000000000248 f
 2 Handle 248
 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 0000000000000248 f】命令查看。

 1 0:000> !handle 0000000000000248 f
 2 Handle 248
 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(等待了)

                    我们再次输入 auto 测试一下 AutoResetEvent。
                    【g】继续运行,提示【选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)】,此次,我们输入 auto,控制台程序输出“are 默认为 false,即等待状态,请查看!”字样。
                    我们在托管堆上查找一下 AutoResetEvent 对象,执行命令【!DumpHeap -type AutoResetEvent】。

1 0:000> !DumpHeap -type AutoResetEvent
2          Address               MT           Size
3     012b87014318     7ff8da3e5f58             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ff8da3e5f58     1        24 System.Threading.AutoResetEvent
8 Total 1 objects, 24 bytes

                    AutoResetEvent 对象的地址是 012b87014318,我们直接使用【!do】或者【!DumpObj】命令查看对象详情。

 1 0:000> !do 012b87014318 
 2 Name:        System.Threading.AutoResetEvent
 3 MethodTable: 00007ff8da3e5f58
 4 EEClass:     00007ff8da3d3638
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da3e3318  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000012b87014330 _waitHandle
11 00007ff8da2870a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    _waitHandle 是应用类型的实例变量,我们继续使用【!do 0000012b87014330】命令查看该类型的详情。

 1 0:000> !do 0000012b87014330
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ff8da3e3318
 4 EEClass:     00007ff8da3d2968
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da2870a0  400126e        8        System.IntPtr  1 instance 00000000000002A4 handle
11 00007ff8da211188  400126f       10         System.Int32  1 instance                4 _state
12 00007ff8da1dd070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ff8da1dd070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    SafeWaitHandle 类型内部又包含了一个 handle 类型对象,值是 00000000000002A4,针对这个值我们可以使用【!dumpvc】查看,也可以使用【!handle】命令查看。

 1 0:000> !handle 00000000000002A4 f
 2 Handle 2a4
 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 Auto Reset(AutoResetEvent)
13     Event is Waiting(False 就是等待)

                    【g】继续运行,控制台程序输出“are 默认为 true,即放行状态,请查看!”字样,再次执行【!handle 00000000000002A4 f】命令。

 1 0:000> !handle 00000000000002A4 f
 2 Handle 2a4
 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 Auto Reset
13     Event is Set

                    【g】继续运行,控制台程序输出“are Reset 后为 false,即等待状态,请查看!”字样,再次执行【!handle 00000000000002A4 f】命令。

 1 0:000> !handle 00000000000002A4 f
 2 Handle 2a4
 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 Auto Reset
13     Event is Waiting
                    我们都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底层的功能,说白了就是 C# 只是使用了 Windows 内核提供的事件,C# 不过是对其进行了包装,如果你想要查看内存地址,必须到内核态去看。
                    我们有了句柄的值了 00000000000002A4,我们需要借助【Process Explorer】工具找到句柄的内核态地址。打开这个工具,然后在【Filter by name】输入项目名称 ExampleCore_6_2,结果如图:

                    

                    我们在【ProcessExplorer】工具下面【Handles】选项中找到我的事件对象,然后双击打开属性对话框,如图:
                    

                    我们就找到了内核地址了。打开一个 Windbg,点击【File】-->【Attach to Kernel】,右侧选择【local】,点击【ok】进入调试器界面。使用【dp 0xFFFF940C4DC47A60】命令。当前值:0(00000000),控制台程序输出“are 默认为 false,即等待状态,请查看!

1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60  00000000`00060001

                    切换到用户态 Windbg 继续【g】运行,控制台程序输出“are 默认为 true,即放行状态,请查看!”字样。回到内核态 Windbg 继续运行【dp 0xFFFF940C4DC47A60】命令。

1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60  00000001`00060001

                    然后,我们再【g】一下【用户态】的 Windbg,控制台输出“are Reset后为 false,即等待状态,请查看!”字样,当前值:0(00000000),然后切换到【内核态】的Windbg,继续使用【dp】命令,查看一下。

1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60  00000000`00060001

                    我们就看到了,状态是0和1相互切换的。

        4.2.2、互斥体(内核锁)
            A、基础知识
                互斥体(Mutex)是一个内核态的同步结构,即可以用于对某个进程内的线程进行同步,也可以在多个进程之间进行同步(通过在创建互斥体时指定名称)。通常来说,如果所有同步操作都位于同一个进程内,那么应该使用监视器对象(Monitor/Lock)或者其他的用户态同步原语。而另一方面,如果需要在多个进程之间进行同步,最合适的就是使用命名互斥体了。
                由于互斥体是一种内核态结构,因此,用户态代码需要 System.Threading.Mutex 来访问互斥体。
                当在用户态中进行调试时,可以使用【!do】或者【!DumpObj】命令来获取关于互斥体更多详细的信息。

                在内核态的数据的 0 表示拥有锁,1 表示释放锁。

                Mutex 类型内部包含了 SafeWaitHandle 引用类型的一个字段 _waitHandle,_waitHandle 类型内部包含了一个值类型的(System.IntPtr)的 handle 实现的同步操作。

            B、眼见为实
                调试源码:ExampleCore_6_3
                调试任务:分别在用户态和内核态两中情况下 Mutex 值的变化。
                由于我们需要在用户态和内核态查看同步对象具体值的变化,需要开启两种调试器,一种是内核态的调试器,一种是用户态的调试器。
                1)、KD 和 NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_3\bin\Debug\net8.0\ExampleCore_6_3.exe】,打开调试器。
                    【g】开始运行我们的调试器,直到调试器输出如图,并进入中断模式,就可以开始我们的调试了。效果如图:
                    

                    我们现在托管堆上查找一下 Mutex 对象,执行【!DumpHeap -type Mutex】命令。

1 0:000> !DumpHeap -type Mutex
2          Address               MT     Size
3 0000013097009628 00007ffef219a190       24
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ffef219a190        1           24 System.Threading.Mutex
8 Total 1 objects

                    红色标注的就是 Mutex 对象的地址 0000013097009628,针对该地址执行【!do 0000013097009628】命令查看详情。

 1 0:000> !do 0000013097009628
 2 Name:        System.Threading.Mutex
 3 MethodTable: 00007ffef219a190
 4 EEClass:     00007ffef21a2ef8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffef219ee70  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000013097009780 _waitHandle
11 00007ffef20c70a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    我们看到了Mutex 类型的内部包含了 SafeWaitHandle 类型的对象 _waitHandle,地址是 0000013097009780,针对该地址继续执行【!do 0000013097009780】命令查看其详情。

 1 0:000> !do 0000013097009780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffef219ee70
 4 EEClass:     00007ffef21a59e8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffef20c70a0  400126e        8        System.IntPtr  1 instance 0000000000000290 handle
11 00007ffef2051188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffef201d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffef201d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    SafeWaitHandle 类型的内部包含了句柄对象 handle,它的值是 0000000000000290,针对该值执行【!handle 0000000000000290 f】命令查看句柄的详情。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type          Mutant
 4   Attributes    0
 5   GrantedAccess 0x1f0001:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState
 8   HandleCount   2
 9   PointerCount  65536
10   Name          <none>
11   Object Specific Information
12     Mutex is Owned(说明已经获取了锁)
13     Mutant Owner b24.de4(这是拥有锁的线程 OSID de4)

                    我们可以使用【!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  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      de4 0000013092951570    2a020 Preemptive  0000013097009EF0:000001309700A610 0000013092992E10 -00001 MTA
11    6    2     23f0 00000130943ADDA0    21220 Preemptive  0000000000000000:0000000000000000 0000013092992E10 -00001 Ukn (Finalizer)
12    7    3     36dc 000001309295D370    2b220 Preemptive  0000000000000000:0000000000000000 0000013092992E10 -00001 MTA
13 0:000>

                    关系如图:
                    

                    我们看到了用户态下 Mutex 值的变化,也需要看看内核态上数据的变化,因此,我们需要借助【Process Explorer】工具。
                    具体操作如图:
                    

                    我们需要双击【ProcessExplorer】下方的【Handles】标红的数据项,打开 Mutex 属性对话框,就能找到内核地址了。
                    

                    在内核态的地址是 0xFFFFD2824D881CD0,有了地址,我们需要打开【KD】内核调试器,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,数据命令【kd -kl】打开调试器,直接执行命令【dp 0xFFFFD2824D881CD0 l1】。

1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0  00000000`00000002

                    Mutex 有了锁,内核数据的值是 00000000。我们需要切换到【NTSD】用户态调试器,继续【g】执行,直到调试器自动进入中断模式。输出如图:
                    

                    说明此时已经释放了锁,再次执行【!handle 0000000000000290 f】查看句柄的变化。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type          Mutant
 4   Attributes    0
 5   GrantedAccess 0x1f0001:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState
 8   HandleCount   2
 9   PointerCount  65534
10   Name          <none>
11   Object Specific Information
12     Mutex is Free(现在已经释放锁了)

                    同样,我们切换到内核【kd】调试器,执行命令【dp 0xFFFFD2824D881CD0 l1】,查看结果。

1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0  00000001`00000002

                    内核态的数据的值现在是 1 了,说明 Mutex 已经释放了锁。


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】调试器,依次点击【文件】---【Launch executable】,加载我们的调试项目:ExampleCore_6_3.exe,进入到调试器。
                    直接使用【g】命令运行调试器,直到我们的控制台程序输出“已进入保护区”字样,调试器也进入了中断模式。
                    我们先在堆上查找一下 Mutex 对象,执行【!DumpHeap -type Mutex】命令。
1 0:000> !DumpHeap -type Mutex
2          Address               MT           Size
3     020ea5409628     7ffecdada190             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffecdada190     1        24 System.Threading.Mutex
8 Total 1 objects, 24 bytes

                    红色标注的 020ea5409628 数据就是 Mutex 对象的地址,然后,执行命令【!do 020ea5409628】,查看 Mutex 详情。

 1 0:000> !do 020ea5409628
 2 Name:        System.Threading.Mutex
 3 MethodTable: 00007ffecdada190
 4 EEClass:     00007ffecdae2ef8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffecdadee70  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000020ea5409780 _waitHandle
11 00007ffecda070a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    我们知道了 Mutex 内部还包含了一个 SafeWaitHandle 类型的 _waitHandle,这个类型是引用类型,我们继续【!do 0000020ea5409780】命令,查看这句柄类型的信息。

 1 0:000> !do 0000020ea5409780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffecdadee70
 4 EEClass:     00007ffecdae59e8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffecda070a0  400126e        8        System.IntPtr  1 instance 00000000000002A0 handle
11 00007ffecd991188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffecd95d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffecd95d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    在 _waitHandle 类型的里面包含了一个值类型的 handle 句柄类型,它的值是 00000000000002A0。有了句柄的值,我们可以使用【!DumpVC 00007ffecda070a0 00000000000002A0】命令查看明细,也可以直接使用【!handle 00000000000002A0 f】命令查看。

 1 0:000> !DumpVC 00007ffecda070a0 00000000000002A0
 2 Name:        System.IntPtr
 3 MethodTable: 00007ffecda070a0
 4 EEClass:     00007ffecd9e6100
 5 Size:        24(0x18) bytes
 6 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 7 Fields:
 8               MT    Field   Offset                 Type VT     Attr            Value Name
 9 00007ffecda070a0  4000525        0        System.IntPtr  1 instance  _value
10 00007ffecda070a0  4000526      a78        System.IntPtr  1   static 0000000000000000 Zero
11 
12 0:000> !handle 00000000000002A0 f
13 Handle 2a0
14   Type             Mutant
15   Attributes       0
16   GrantedAccess    0x1f0001:
17          Delete,ReadControl,WriteDac,WriteOwner,Synch
18          QueryState
19   HandleCount      2
20   PointerCount     65536
21   Name             <none>
22   Object Specific Information
23     Mutex is Owned(进入锁状态)
24     Mutant Owner 3438.3b78(持有 Mutex 线程的 ID 3b78)

                    我们可以使用【!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  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     3b78 0000020EA0D37F10    2a020 Preemptive  0000020EA5409EF0:0000020EA540A610 0000020ea0d79770 -00001 MTA 
11    5    2     4210 0000020EA0DE7C40    21220 Preemptive  0000000000000000:0000000000000000 0000020ea0d79770 -00001 Ukn (Finalizer) 
12    6    3     1ef0 0000020EA0D43DC0    2b220 Preemptive  0000000000000000:0000000000000000 0000020ea0d79770 -00001 MTA 

                    效果如图:
                    

                    此时,我们可以使用【Process Explorer】工具查找一下 Mutex 对象在内核态上的地址,看看内核态地址上的内容的变化。我们打开【Process Explorer】,如图操作:
                    

                    我们点击【ProcessExplorer】工具【Handles】选项,双击 Mutant 打开属性对话框。效果如图:
                    

                    我们找到了内核中的数据的地址 0xFFFFD2824D1A5BB0,此时,我们需要再重新打开另外一个【Windbg Preview】,依次点击【文件】---【Attach to kernel】,在右侧选择【local】,进入到调试器。
                    继续执行命令【dp 0xFFFFD2824D1A5BB0 l1】命令,看看内核数据是怎么表示的。

1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0  00000000`00000002

                    此时,我们再次切换到用户态的【Windbg Preview】,【g】继续运行调试器,控制台程序会输出“正在离开保护区”的字样。我们继续执行【!handle 00000000000002A0 f】命令,看看是什么结果。

 1 0:000> !handle 00000000000002A0 f
 2 Handle 2a0
 3   Type             Mutant
 4   Attributes       0
 5   GrantedAccess    0x1f0001:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState
 8   HandleCount      2
 9   PointerCount     65534
10   Name             <none>
11   Object Specific Information
12     Mutex is Free(已经释放了锁)

                    已经执行了 ReleaseMutex 方法了,所以就是释放了锁了。
                    此时,我们再次切换到内核态的【Windbg Preview】,继续执行【dp 0xFFFFD2824D1A5BB0 l1】命令,结果如下:

1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0  00000001`00000002

                    此时,内核态的数据已经变成 1 了。也就是说在内核态的数据的 0 表示拥有锁,1 表示释放锁。


        4.2.3、信号量(内核锁)
            A、基础知识
                Semaphore(信号量)是一种内核态的同步对象,可以在用户态访问。它类似 Mutex(互斥体),可以实现对资源的互斥访问。它们的区别在于,信号量采用了资源计数,因此可以同时允许 X 个线程访问这个资源。
                AutoResetEvent、ManulResetEvent 维护的是 bool 类型的值,信号量本质上就是维护了一个 int 值,这就是两者的区别,我们可以使用 Windbg 来查看一下 waitHandle 的值,可以发现 Semaphore 的 Count 的值在不断的变化。
                Semaphore(信号量)可以使用【!do】或者【!DumpObj】命令查看对象信息,也可以使用【!handle】命令查看句柄的信息。
  
                Semaphore 类型内部包含了 SafeWaitHandle 引用类型的一个字段 _waitHandle,_waitHandle 类型内部包含了一个值类型的(System.IntPtr)的 handle 实现的同步操作。

            B、眼见为实
                调试源码:ExampleCore_6_4
                调试任务:分别在用户态和内核态看 Semaphore 值的变化。
                1)、KD 和 NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_4\bin\Debug\net8.0\ExampleCore_6_4.exe】,打开调试器。
                    进入调试器后,就可以执行【g】命令运行调试器,直到调试器输出如图就可以开始调试了。
                    

                    我们现在托管堆上查找一下Semaphore 对象,直接执行【!DumpHeap -type Semaphore】命令。

1 0:000> !DumpHeap -type Semaphore
2          Address               MT     Size
3 000002754fc09628 00007ffa1ed0a198       24
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ffa1ed0a198        1           24 System.Threading.Semaphore
8 Total 1 objects

                    我们知道了 Semaphore 对象的地址是 000002754fc09628,然后执行【!do 000002754fc09628】命令。

 1 0:000> !do 000002754fc09628
 2 Name:        System.Threading.Semaphore
 3 MethodTable: 00007ffa1ed0a198
 4 EEClass:     00007ffa1ed12ea8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa1ed31148  4000b7a        8 ...es.SafeWaitHandle  0 instance 000002754fc09780 _waitHandle
11 00007ffa1ec370a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    System.Threading.Semaphore 类型内部包含了一个 SafeWaitHandle 类型的域 _waitHandle,该 _waitHandle 类型的地址是 000002754fc09780,我们有了地址,继续执行【!do 000002754fc09780】命令查看它的详情。

 1 0:000> !do 000002754fc09780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffa1ed31148
 4 EEClass:     00007ffa1ed16bb8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa1ec370a0  400126e        8        System.IntPtr  1 instance 0000000000000290 handle
11 00007ffa1ebc1188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffa1eb8d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffa1eb8d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    Microsoft.Win32.SafeHandles.SafeWaitHandle 类型内部包含了 System.IntPtr 类型一个域 handle,它的值是 0000000000000290,有了这个值,我们就可以使用【!handle 0000000000000290 f】命令查看句柄的详情了。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type          Semaphore
 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     Semaphore Count 2(当前计数是2,每次执行都会累加)
13     Semaphore Limit 10(这是最大值,超过就会抛出异常)

                    内容很简单,就不做过多解释了。这个句柄的值  0000000000000290 要记住,后面找内核地址要使用这个。
                    我们想要找到句柄的内核地址,必须 借助【ProcessExplorer】工具,操作如图:
                    

                    双击【ProcessExloprer】下方【Handles】的 Semaphore 记录,打开详情,内核地址就在里面。
                    

                    handle 句柄的内核地址是 0xFFFFA68F9E3CE2E0,有了地址,我们就可以使用【kd】内核调试器显示数据内容了。
                    打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【kd -kl】打开调试器,执行命令【!dp 0xFFFFA68F9E3CE2E0 l4】。效果如图:
                    

                    我们再次切换到用户态的【NTSD】调试器中,执行【g】命令和【!handle 0000000000000290 f】,查看变化。

 1 0:000> g
 2 查看当前的 sem 值。
 3 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
 4 KERNELBASE!wil::details::DebugBreak+0x2:
 5 00007ffb`4129b502 cc              int     3
 6 
 7 0:000> !handle 0000000000000290 f
 8 Handle 290
 9   Type          Semaphore
10   Attributes    0
11   GrantedAccess 0x1f0003:
12          Delete,ReadControl,WriteDac,WriteOwner,Synch
13          QueryState,ModifyState
14   HandleCount   2
15   PointerCount  65534
16   Name          <none>
17   Object Specific Information
18     Semaphore Count 3(第一次执行是2,现在是 3,每次执行都会递增)
19     Semaphore Limit 10(最大值)

                    我们再切换到内核态【kd】调试器上,执行【dp 0xFFFFA68F9E3CE2E0 l4】命令。

1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0  00000003`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0  ffffa68f`9e3ce2e8 00000000`0000000a

                    数值已经变为为 3 了,和用户态调试器输出是一致的。我们可以重复多次,每次查看变化,很简单,我就省略了。
                    我在用户态下执行执行到计数数字 10,然后在执行,看看会不会发生异常。

 1 0:000> g
 2 查看当前的 sem 值。
 3 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
 4 KERNELBASE!wil::details::DebugBreak+0x2:
 5 00007ffb`4129b502 cc              int     3
 6 
 7 0:000> g
 8 查看当前的 sem 值。
 9 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
10 KERNELBASE!wil::details::DebugBreak+0x2:
11 00007ffb`4129b502 cc              int     3
12 
13 0:000> g
14 查看当前的 sem 值。
15 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
16 KERNELBASE!wil::details::DebugBreak+0x2:
17 00007ffb`4129b502 cc              int     3
18 
19 0:000> g
20 查看当前的 sem 值。
21 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
22 KERNELBASE!wil::details::DebugBreak+0x2:
23 00007ffb`4129b502 cc              int     3
24 
25 0:000> g
26 查看当前的 sem 值。
27 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
28 KERNELBASE!wil::details::DebugBreak+0x2:
29 00007ffb`4129b502 cc              int     3
30 
31 0:000> g
32 查看当前的 sem 值。
33 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
34 KERNELBASE!wil::details::DebugBreak+0x2:
35 00007ffb`4129b502 cc              int     3
36 
37 0:000> g
38 查看当前的 sem 值。
39 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
40 KERNELBASE!wil::details::DebugBreak+0x2:
41 00007ffb`4129b502 cc              int     3
42 
43 0:000> !handle 0000000000000290 f
44 Handle 290
45   Type          Semaphore
46   Attributes    0
47   GrantedAccess 0x1f0003:
48          Delete,ReadControl,WriteDac,WriteOwner,Synch
49          QueryState,ModifyState
50   HandleCount   2
51   PointerCount  65527
52   Name          <none>
53   Object Specific Information
54     Semaphore Count 10
55     Semaphore Limit 10

                    我们在看看内核态数据的变化,切换到【kd】调试器上,执行命令【dp 0xFFFFA68F9E3CE2E0 l4】。

1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0  0000000a`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0  ffffa68f`9e3ce2e8 00000000`0000000a

                    我们看到内核态的值已经变成 0000000a 了。
                    我们回到用户态的【NTSD】调试器,继续【g】,看看会发生什么。

1 0:000> g
2 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000   C:\Windows\SYSTEM32\icu.dll
3 (23a8.1c70): CLR exception - code e0434352 (first chance)
4 (23a8.1c70): CLR exception - code e0434352 (!!! second chance !!!)
5 KERNELBASE!RaiseException+0x69:
6 00007ffb`411dcf19 0f1f440000      nop     dword ptr [rax+rax]

                    我们看到发生了 CLR exception 异常了,和我们期望的一样。


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_6_4.exe,直接进入调试器。
                    进入到调试器后,【g】直接运行调试器,我们的控制台程序会输出“查看当前的 sem 值。”字样,调试器会自动进入中断模式,此时,就可以开始我们的调试了。
                    我们先在托管堆上查找一下 Semaphore 对象是否存在,执行命令【!DumpHeap -type Semaphore】。
1 0:000> !DumpHeap -type Semaphore
2          Address               MT           Size
3     027685409628     7ffa06f8a198             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffa06f8a198     1        24 System.Threading.Semaphore
8 Total 1 objects, 24 bytes

                    我们找到了 Semaphore 对象的地址,有了地址就好办了,我们直接执行【!do 027685409628】命令,查看它的详情。

 1 0:000> !do 027685409628
 2 Name:        System.Threading.Semaphore
 3 MethodTable: 00007ffa06f8a198
 4 EEClass:     00007ffa06f92ea8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa06fb1148  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000027685409780 _waitHandle
11 00007ffa06eb70a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    System.Threading.Semaphore 内部包含了一个 SafeWaitHandle 类型的 _waitHandle 域,针对该域我们使用【!do 0000027685409780】命令,查看 _waitHandle 的详情。

 1 0:000> !do 0000027685409780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffa06fb1148
 4 EEClass:     00007ffa06f96bb8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa06eb70a0  400126e        8        System.IntPtr  1 instance 0000000000000290 handle
11 00007ffa06e41188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffa06e0d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffa06e0d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    Microsoft.Win32.SafeHandles.SafeWaitHandle 内部包含了一个 System.IntPtr 类型的域 handle。我们有了 handle 的值 0000000000000290,就可以使用命令【!handle 0000000000000290 f】查看这个句柄的详情了。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type             Semaphore
 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     Semaphore Count 2(当前的计数,初始值我们设置的是 1)
13     Semaphore Limit 10(这个是极限值,超过会抛出异常)

                    这些都是在用户态调试器下的显示,我们也要看看在内核态下是怎么显示的,记住 handle 的值,后面会用到。
                    我们想要在内核态想查看数据的变化,必须找到句柄的内核态地址,所以我们要借助【ProcessExplorer】工具,操作如图:
                    

                    我们在【ProcessExplorer】下方的【Handles】找到 Semaphore 信号量对象,继续双击就可以看到它的内核态的地址。
                    

                    很简单,就不多说了,我们知道了它的内核地址 0xFFFFA68F9E3E1CE0。此时,我们需要在打开一个【Windbg Preview】,依次点击【文件】----【Attach to kernel】,在窗口的右侧选择【local】,点击【ok】进去调试器,就可以使用【dp 0xFFFFA68F9E3E1CE0 l4】命令查看数据了。

1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0  00000002`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0  ffffa68f`9e3e1ce8 00000000`0000000a

                    00000002 就是当前值,00000000`0000000a 就是极限值。
                    接下来就简单了,我们多次执行用户态的调试器,然后再在内核态调试器里查看变化,一目了然。
                    我先执行一次用户态下【g】命令,在执行【!handle 0000000000000290 f】命令,查看变化。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 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 3(上一次是2,此次是3)
13     Semaphore Limit 10

                    我们在切换到内核态调试器中执行【dp 0xFFFFA68F9E3E1CE0 l4】命令。

1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0  00000003`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0  ffffa68f`9e3e1ce8 00000000`0000000a

                    00000003 变为 3了。
                    我们可以继续连续执行同样的命令,查看结果。
                    当我在用户态执行的时候,当当前计数大于10的时候,会发生异常。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type             Semaphore
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65527
10   Name             <none>
11   Object Specific Information
12     Semaphore Count 10
13     Semaphore Limit 10
14 
15 0:000> g
16 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000   C:\Windows\SYSTEM32\icu.dll
17 (3a8c.940): CLR exception - code e0434352 (first chance)
18 (3a8c.940): CLR exception - code e0434352 (!!! second chance !!!)
19 KERNELBASE!RaiseException+0x69:
20 00007ffb`411dcf19 0f1f440000      nop     dword ptr [rax+rax]

                    我们在看看内核态的数据,继续执行命令。

1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0  0000000a`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0  ffffa68f`9e3e1ce8 00000000`0000000a

                    当前的计数值就是 10(十六进制 0xa) 了。


        4.2.4、监视器(混合锁)
            A、基础知识
                监视器是一种对某个对象的访问操作进行监视的结构,它能在对象上创建一个锁,因而只有当持有该监视器对象的线程离开监视器对象后,其他线程才能访问。
                监视器和其他同步原语不同,它不是对内核 Windows 同步原语进行是简单的封装,而是在 .NET 中定义的类,即:System.Threading.Monitor,Monitor 类不能实例化,而是包含了一组静态方法,用于获取一个锁。Enter 和 Exit 是很常用的两个方法,Enter 用于获取指定对象上的互斥锁,Exit 用于释放指定对象上的互斥锁。
                lock 关键字就是对 Monitor 对象的封装,lock 语句会自动进入一个监视器,并将保护区域内的代码封装在一个 try/finally 块中,以确保监视器在作用域结束后释放锁。
                由于 Monitor 类是一个不能被实例化的对象,因此无法看到它的任何状态,锁的信息保存在被锁定的对象中。
                监视器是由 C# 中的 AwareLock 实现的,底层是基于 AutoResetEvent 机制,可以参见 coreclr 源码。因为 Monitor 是基于对象头的同步块索引来实现的,我们可以查看对象头的数据结构就可以明白了。
                
            B、眼见为实
                调试源码:ExampleCore_6_5
                调试任务:我们使用 Windbg 查看 Monitor 的实现
                1)、NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_5\bin\Debug\net8.0\ExampleCore_6_5.exe】打开调试器。【g】直接运行调试器,调试器会输出“4 已进入 Person 锁中 111111”字样,自动进入中断模式,现在,就可以开始我们的调试了。如图:
                    
                    因为我们知道是锁的问题,所以可以直接执行【!syncblk】命令。
1 0:008> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     2 0000015A8405C070            3         1 0000015A8404A830 38f0   8   00000119f200c9f8 ExampleCore_6_5.Person
4 -----------------------------
5 Total           3
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    我们说过 Monitor 的底层实现就是 AwareLock,这个标红 0000015A8405C070 地址就是指向  AwareLock。我们使用【dt coreclr!AwareLock 0000015A8405C070】命令查看一番。

 1 0:008> dt coreclr!AwareLock 0000015A8405C070
 2    +0x000 m_lockState      : AwareLock::LockState(这里就说明了 Monitor 底层是 AwareLock)
 3    +0x004 m_Recursion      : 1
 4    +0x008 m_HoldingThread  : 0x0000015a`8404a830 Thread(持有锁的托管线程标识,和 !synck 输出  Owning Thread Info 列的前部分一致)
 5    +0x010 m_HoldingOSThreadId : 0x38f0(持有锁的操作系统线程标识,和 !synck 输出  Owning Thread Info 列的后部分一致
 6    +0x018 m_TransientPrecious : 0n1
 7    +0x01c m_dwSyncIndex    : 0x80000002(同步块的索引值,和 !synck 输出的 Index 值一样)
 8    +0x020 m_SemEvent       : CLREvent(这里说明,底层还是使用了 Event 同步原语,如果在 Windbg 里是可以点击的,这里没办法了)
 9    +0x030 m_waiterStarvationStartTimeMs : 0x10c6663
10    +0x034 m_emittedLockCreatedEvent : 0n0

                    我们继续使用【dx -r1 (*((coreclr!CLREvent *) XXXXXXXXX))】命令查看 m_SemEvent 是什么。XXXXXXXXX 是 m_SemEvent 的地址,我没有算出来,下面的步骤就没办法进行了。在【Windbg Preview】里是直接可以点击查看的,这就是【Windbg】和 命令行工具的区别。


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_6_5.exe,进入到调试器。
                    我们使用【g】命令,继续运行调试器,我们的控制台程序输出:6 已进入 Person 锁中 222222(这里不一定是这个,我的输出是这个),Windbg 有一个 int 3 中断,就可以调试程序了。
                    然后,我们使用【!syncblk】命令,查看一下同步块。
1 0:009> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     2 00000217A549CE10            3         1 00000217A54963A0 26c   9   000001d713010a28 ExampleCore_6_5.Person
4 -----------------------------
5 Total           2
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    我们说过 Monitor 的底层实现就是 AwareLock,这个标红 00000217A549CE10 地址就是指向  AwareLock。我们使用【dt coreclr!AwareLock 00000217A549CE10】命令查看一番。

 1 0:009> dt coreclr!AwareLock 00000217A549CE10
 2    +0x000 m_lockState      : AwareLock::LockState(底层的 awarelock)
 3    +0x004 m_Recursion      : 1
 4    +0x008 m_HoldingThread  : 0x00000217`a54963a0 Thread(持有锁的线程的标识,也就是!syncblk 命令输出的 Owning Thread Info 列的值前部分(00000217A54963A0) 5    +0x010 m_HoldingOSThreadId : 0x26c(持有锁的操作系统线程标识也就是!syncblk 命令输出的 Owning Thread Info 列的值后部分(26c)
 6    +0x018 m_TransientPrecious : 0n1
 7    +0x01c m_dwSyncIndex    : 0x80000002(这个就是同步块索引,也就是!syncblk 命令输出的 Index 列的值)
 8    +0x020 m_SemEvent       : CLREvent(底层还是使用的 Event 实现同步)
 9    +0x030 m_waiterStarvationStartTimeMs : 0xf4b013
10    +0x034 m_emittedLockCreatedEvent : 0n0

                    我们继续使用【dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))】命令查看 m_SemEvent 是什么,不用执行命令,直接点击就可以了。

1 0:009> dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))
2 (*((coreclr!CLREvent *)0x217a549ce30))                 [Type: CLREvent]
3     [+0x000] m_handle         : 0x314 [Type: void *](这里是一个句柄)
4     [+0x008] m_dwFlags        : 0xd [Type: Volatile<unsigned long>]

                    既然是一个 handle,我们就使用【!handle 0x314 f】命令查看一下就知道了。

 1 0:009> !handle 0x314 f
 2 Handle 314
 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
13     Event is Waiting

                  我们看到了吧,Monitor 底层也是使用 AutoResetEvent 实现的。


        4.2.5、读写锁(ReaderWriterLock)
            A、基础知识
                Monitor 类每次只允许一个线程独占式的访问一个对象。虽然,在写入操作非常频繁的情况下,Monitor 能工作的很好,但当读取操作多于写操作或者在锁上存在高度竞争的情况下,Monitor 的性能就很受影响了。
                为了解决这个问题,系统为我们提供了读写锁,即 ReaderWriterLock 。ReaderWriterLock 能够使多个线程并发的执行读操作,而每次只允许一个线程执行写操作。ReaderWriterLock 类本身就包含了状态来控制对锁的访问。
                ReaderWriterLock 用于同步对资源的访问。 在任何给定时间,它都允许对多个线程进行并发读取访问,或对单个线程进行写入访问。 在资源不经常更改的情况下, ReaderWriterLock 与简单的一次锁定(例如 ) Monitor相比,提供更好的吞吐量。
                ReaderWriterLock 在大多数访问是读取,而写入不频繁且持续时间短的情况下,效果最佳。 多个读取器交替使用单个编写器,因此读取器和编写器都不会长时间被阻止。线程可以同时持有读取器锁或编写器锁,但不能同时持有两者。

                长时间保留读取器锁或编写器锁会耗尽其他线程。 为了获得最佳性能,请考虑重新调整应用程序以最大程度地减少写入持续时间。
                在 Net 平台上,有两类读写锁可用:ReaderWriterLockReaderWriterLockSlim,如果想了解详情,可以去微软官网学习。

                注意:
                  .NET Framework 有两个读取器-写入器锁和 ReaderWriterLockSlim、ReaderWriterLock。 建议对所有新开发的项目使用 ReaderWriterLockSlim。 虽然 ReaderWriterLockSlim 类似于 ReaderWriterLock,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。 ReaderWriterLockSlim 避免了许多潜在的死锁情况。 另外,ReaderWriterLockSlim 的性能显著优于 ReaderWriterLock。

            B、眼见为实
                调试源码:ExampleCore_6_6
                调试任务:使用调试器从底层了解 ReaderWriterLock 到底是什么。
                1)、NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_6\bin\Debug\net8.0\ExampleCore_6_6.exe】直接进入调试器。
                    直接【g】运行调试器,直到调试器输出“Press ENTER to exit...”字样时,按组合键【ctrl+c】进入中断模式,开始调试了。
                    我们现在托管堆上查找一下 ReaderWriterLock 对象,执行【!DumpHeap -type ReaderWriterLock】命令。
1 0:003> !DumpHeap -type ReaderWriterLock
2          Address               MT     Size
3 000001354f409848 00007ff9e50c75e8       56
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ff9e50c75e8        1           56 System.Threading.ReaderWriterLock
8 Total 1 objects

                    标红的 000001354f409848 就是 ReaderWriterLock 对象的地址,继续执行【!do 000001354f409848】命令,查看它的详情。

 1 0:003> !do 000001354f409848
 2 Name:        System.Threading.ReaderWriterLock
 3 MethodTable: 00007ff9e50c75e8
 4 EEClass:     00007ff9e50aa388
 5 Tracked Type: false
 6 Size:        56(0x38) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff9e4f593f0  400001d        8          System.Void  0 instance 0000000000000000 _readerEvent
11 00007ff9e4f593f0  400001e       10          System.Void  0 instance 0000000000000000 _writerEvent
12 00007ff9e4f7a5f0  400001f       18         System.Int64  1 instance                1 _lockID
13 00007ff9e4f51188  4000020       20         System.Int32  1 instance                0 _state
14 00007ff9e4f51188  4000021       24         System.Int32  1 instance               -1 _writerID
15 00007ff9e4f51188  4000022       28         System.Int32  1 instance                1 _writerSeqNum
16 00007ff9e4f767b8  4000023       2c        System.UInt16  1 instance                0 _writerLevel
17 00007ff9e4f51188  400001b       58         System.Int32  1   static              500 DefaultSpinCount
18 00007ff9e4f7a5f0  400001c       50         System.Int64  1   static                1 s_mostRecentLockID

                    _readerEvent_writerEvent 是指针类型,分别用来控制对读取队列和写入队列的访问。_state 表示锁的各种不同的内部状态。_lockID 持有锁线程的内部标识。_writerID 持有锁线程的 ID,_writerLevel 持有写入线程的递归锁计数(Recursive lock count)。


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】调试器,依次点击【文件】---【Launch executable】,加载我们的项目文件 ExampleCore_6_6.exe,直接进入调试器。执行【g】命令,运行调试器,直到我们的控制台程序输出“Press ENTER to exit...”字样,然后点击调试器的【break】按钮,进入中断状态,现在开始我们的调试吧。
                    我们现在托管堆上查找一下 ReaderWriterLock 对象,执行【!DumpHeap -type ReaderWriterLock】命令。
1 0:006> !DumpHeap -type ReaderWriterLock
2          Address               MT           Size
3     022afb409848     7ffa021b7788             56 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffa021b7788     1        56 System.Threading.ReaderWriterLock
8 Total 1 objects, 56 bytes

                    红色标注的 022afb409848 就是 ReaderWriterLock 对象的地址,有了地址,我们执行【!do 022afb409848】命令。

 1 0:006> !do 022afb409848
 2 Name:        System.Threading.ReaderWriterLock
 3 MethodTable: 00007ffa021b7788
 4 EEClass:     00007ffa0219a4f0
 5 Tracked Type: false
 6 Size:        56(0x38) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa020493f0  400001d        8          System.Void  0 instance 0000000000000000 _readerEvent
11 00007ffa020493f0  400001e       10          System.Void  0 instance 0000000000000000 _writerEvent
12 00007ffa0206a5f0  400001f       18         System.Int64  1 instance                1 _lockID
13 00007ffa02041188  4000020       20         System.Int32  1 instance                0 _state
14 00007ffa02041188  4000021       24         System.Int32  1 instance               -1 _writerID
15 00007ffa02041188  4000022       28         System.Int32  1 instance                1 _writerSeqNum
16 00007ffa020667b8  4000023       2c        System.UInt16  1 instance                0 _writerLevel
17 00007ffa02041188  400001b       58         System.Int32  1   static              500 DefaultSpinCount
18 00007ffa0206a5f0  400001c       50         System.Int64  1   static                1 s_mostRecentLockID

                    _readerEvent 和 _writerEvent 是指针类型,分别用来控制对读取队列和写入队列的访问。_state 表示锁的各种不同的内部状态。_lockID 持有锁线程的内部标识。_writerID 持有锁线程的 ID,_writerLevel 持有写入线程的递归锁计数(Recursive lock count)。


        4.2.6、线程池
            A、基础知识
                创建新线程的方式很多,比如:Thread、ThreadPool、Task、Parallel 等,除了 Thread 类,其他都是使用了线程池技术,让 CLR 来高效的管理这个线程池,所以,.NET 开发建议使用具有线程池的类型。每个进程有且只有一个线程池。需要注意一点,当线程被还回线程池时,在线程上设置的任何状态都会保留下来。如果同一个线程被用于服务另一个任务请求,并且该任务请求与线程状态不兼容,那么程序可能会失败。
                我们可以使用 ThreadPool 的 GetMaxThreads 方法来获取线程池的最大工作者线程数量和 I/O 线程的数量,使用 GetMinThreads 方法获取线程池最小的工作者线程数量和 I/O 线程的数量。当然有获取操作就有设置操作,可以使用 SetMinThreads 方法设置线程池工作者线程和 I/O 线程的最小值,最小值是 1(在 Net Framework 是 0),可以使用 SetMaxThreads 方法设置线程池的工作者线程和 I/O 线程的最大值,32767 是最大值,如果超过最大值就会使用最大值初始化工作者线程的数量和 I/O 线程的数量。
                在 Net Framework 环境下,默认的线程池最大工作者线程是 2047,最大 I/O 线程的数量是 1000。默认的线程池最小的工作者线程数量与 I/O线程数量和 CPU 的核数一致。如果设置的线程池中线程的数量超过最大值 32767,将使用 32767 作为默认值,如果设置线程池中线程的数量小于最小值(最小值是 0),将使用和 CPU 核数一致的数字初始化线程的数量(工作者线程的数量和 I/O 线程的数量)。
                在 Net 跨平台版本中,默认的线程池最大工作者线程数量是 32767,默认的最大 I/O 线程的数量是 1000。默认的线程池最小的工作者线程数量和 CPU 的核数一致,默认的线程池最小 I/O线程数量 1。如果设置的线程池中线程的数量超过最大值 32767,将使用 32767 作为最大值,无论是设置工作者线程还是 I/O线程数量,有一个是负值,工作者线程将使用和 CPU 核数一致的数字初始化线程的数量,I/O 线程的数量就会初始化为 1。如果工作者线程的数量和 I/O 线程的数量设置 0,系统就会使用 1 初始化这两个线程的数量。
                线程池在 Net Framework 和 Net 上还是有区别的,区别就是设置最小值的情况,Net Framework 平台,无论是工作者线程还是 I/O线程,可以设置为 0,但是在 Net 跨平台版本最小值是不能设置为 0,最小值为 1。
                要在调试过程中找出线程池的状态,可以使用SOS调试器扩展命令【!threadpool】。

        B、眼见为实
            调试源码:ExampleCore_6_6_ThreadPool 和 ExampleCore_6_6_ThreadPool.Net(Net Framework 4.8)
            调试任务:熟悉【!threadpool】命令的使用
                1)、NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.10.4】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_6_ThreadPool\bin\Debug\net8.0\ExampleCore_6_6_ThreadPool.exe】,回车进入调试器。
                    进入调试器后,直接运行【g】命令。
 1 0:000> g
 2 ModLoad: 00007fff`3ee40000 00007fff`3ee72000   C:\Windows\System32\IMM32.DLL
 3 ModLoad: 00007ffe`c2020000 00007ffe`c2079000   C:\Program Files\dotnet\host\fxr\8.0.7\hostfxr.dll
 4 ModLoad: 00007ffe`c1fb0000 00007ffe`c2014000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\hostpolicy.dll
 5 ModLoad: 00007ffe`57ad0000 00007ffe`57fb5000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\coreclr.dll
 6 ModLoad: 00007fff`3ea30000 00007fff`3eb5b000   C:\Windows\System32\ole32.dll
 7 ModLoad: 00007fff`3f4d0000 00007fff`3f823000   C:\Windows\System32\combase.dll
 8 ModLoad: 00007fff`3f400000 00007fff`3f4cd000   C:\Windows\System32\OLEAUT32.dll
 9 ModLoad: 00007fff`3dc70000 00007fff`3dcf2000   C:\Windows\System32\bcryptPrimitives.dll
10 (4250.4754): Unknown exception - code 04242420 (first chance)
11 ModLoad: 00007ffe`38cf0000 00007ffe`3997d000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Private.CoreLib.dll
12 ModLoad: 00007ffe`58650000 00007ffe`58809000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\clrjit.dll
13 ModLoad: 00007fff`3dc50000 00007fff`3dc62000   C:\Windows\System32\kernel.appcore.dll
14 ModLoad: 00000214`1e850000 00000214`1e858000   E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_6_ThreadPool\bin\Debug\net8.0\ExampleCore_6_6_ThreadPool.dll
15 ModLoad: 00000214`1e860000 00000214`1e86e000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Runtime.dll
16 ModLoad: 00000214`1e870000 00000214`1e878000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Threading.ThreadPool.dll
17 ModLoad: 00007fff`089f0000 00007fff`08a18000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Console.dll
18 ModLoad: 00007fff`04aa0000 00007fff`04ab2000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Threading.dll
19 ModLoad: 00000214`1e880000 00000214`1e888000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Text.Encoding.Extensions.dll
20 ModLoad: 00007ffe`eee00000 00007ffe`eee15000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.7\System.Runtime.InteropServices.dll
21 修改前-GetMaxThreads:workderThreads=32767,completionPortThreads=1000
22 修改前-GetMinThreads:workderThreads=4,completionPortThreads=1
23 修改后-SetMinThreads-GetMinThreads:workderThreads=1,completionPortThreads=1
24 修改后-SetMaxThreads-GetMaxThreads:workderThreads=32767,completionPortThreads=32767
25 ThreadPool.QueueUserWorkItem 开始执行
26 Task.Run 开始执行

                    调试器暂停,我们按【ctrl+c】组合键,进入中断模式,继续执行【!threadpool】命令。

1 0:011> !threadpool
2     Failed to request ThreadpoolMgr information

                    出错了,使用【NTSD】调试就会出这些奇怪的问题,原因我不知道。但是使用 Windbg 调试就没问题,只能慢慢研究吧。


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】调试器,依次点击【文件】---【Launch executable】,加载我们的项目 EXE 文件,直接进入调试器。
                    进入调试器后,直接运行【g】命令,我们的控制台程序输出如图:

                    

                    回到调试器界面,点击【Break】按钮,让我们的调试器进入中断模式,然后直接执行【!threadpool】命令,就可以查看线程池的具体情况了。

1 0:007> !threadpool
2 Using the Portable thread pool.
3 
4 CPU utilization:  56%
5 Workers Total:    3(工作者线程总数)
6 Workers Running:  2(正在运行的工作者线程数)
7 Workers Idle:     1(空闲的工作者线程数量)
8 Worker Min Limit: 1(工作者线程最小的数量值
9 Worker Max Limit: 32767(工作者最大线程数量值)

                    以上是在 .Net 8.0 上输出的,在 Net Framework 输出略有不同。

1 0:007> !threadpool
2 CPU utilization: 54%
3 Worker Thread: Total: 3 Running: 2 Idle: 1 MaxLimit: 32765 MinLimit: 4
4 Work Request in Queue: 0
5 --------------------------------------
6 Number of Timers: 0
7 --------------------------------------
8 Completion Port Thread:Total: 0 Free: 0 MaxFree: 8 CurrentLimit: 0 MaxLimit: 32765 MinLimit: 4

                    【!threadpool】命令输出的第一部分是工作线程的统计信息,即当前CPU的使用率(54%)总工作线程数量(3)、正在运行的工作线程数量(2)以及空闲工作线程的数量(1)。此外它还给出了线程池的最大线程数和最小线程数(32765 和4)。接下来的一行是队列中正在等待被服务的请求数量(0)。如果程序正在使用线程池来维持计时器,那么还会给出计时器的总数(0)。在输出的最后一部分给出了I/O完成端口线程上的数据。                    
                    内容很简单,就不多说了。

    4.3、同步的内部细节
        4.3.1、对象头
            在托管堆上保存的每个对象都包含一个对象头,在对象头中包含了与对象相关的一组信息。在对象头中可以包含包括散列码、锁信息、同步块索引等。如图所示:
            
            在对象中需要保存的所有信息总量大于对象头本身的大小。这句话的意思,任何一个对象都可能需要(也可能不需要)所有的信息,这取决于具体的执行流程。只要在执行操作中需要的信息(例如:对象的散列码)不超过对象头的大小,这些信息就会直接保存在对象头中。如果对象头中无法保存所需的信息,CLR 会创建一个独立的同步块数据结构,并将当前保存在对象头中的所有信息都复制到这个同步块中,并且,将对象头中保存的信息替换成同步块在同步块表中的索引。同步块位于非 GC 的内存中,通过同步块表中的索引来访问。

            CLR 通过对象头中的位元的组织方式区分对象头中包含的信息的种类。如果在对象头中设置了掩码 0x08000000,就表示对象头中包含要么是对象的散列码,要么是同步块索引。如果同时设置了掩码 0x04000000,就表示对象头中保存的是散列码。
            我们使用 哈希码(16进制)+0x08000000+0x04000000=对象头的值,就能说明对象头存储的是哈希值。
            也可以通过:对象头的值-哈希码=0x0c000000,也能表明存储的是散列码。


        4.3.2、同步块
            A、基础知识
                这一节主要是验证对象头保存数据的方式,例如:如何保存锁信息,如何保存散列码等信息。和同步块相关的有一个命令很重要,就是【!syncblk】,如果该命令不携带任何参数,表示它将输出某个线程中所有对象的同步块。当然,我们也可以将同步块的索引值作为参数,输出指定同步块的信息。
                请记住,对象指针指向的是类型句柄域,紧接着才是实际的对象数据。在类型句柄前的 4 或者 8 个字节也是对象布局的一部分,其中就包含了对象头,所以,如果我们想找到对象头,就要使用对象的地址减去 4 或者 8 个字节(32位减去4字节,4 字节就是 0x4,64位减去8字节,8字节就是 0x8)就是对象头的数据。

                如果我们想得到同步块索引,可以执行如下操作:
                1)、通过使用【!ClrStack -a】命令输出这个线程的所有的调用栈及其所有参数和局部变量。最底层的栈帧对应于 Main 方法。
                2)、继续使用【!do】命令,确认是否是我们需要的对象。
                3)、最后使用【dp】命令输出对象头,它位于对象指针减去 4 或者 8 个字节(32位减去4字节,4 字节就是 0x4,64位减去8字节,8字节就是 0x8)的位置上。

                接下来,我们在说说【!syncblk】命令各列的意思。
                Index:同步块索引
                SyncBlock:同步块数据结构的地址(未公开)
                MonitorHeld:持有的监视器的数量
                Recursion:同一个线程获取这个锁的次数
                Owning thread info:第一个数据项是指向内部线程数据结构的指针,第二个数据项是操作系统线程ID,第三个数据项是调试器线程ID
                SyncBlock Owner:第一个数据项是指向持有锁的对象的指针,第二个数据项是锁所在的对象的类型

            B、眼见为实
                调试源码:ExampleCore_6_7
                调试任务:通过调试器了解对象头保存数据的方式。
                1)、NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.exe】打开调试器。
                    进入调试器后,直接【g】运行调试器,直到调试器输出如图:
                    

                    此时,我们按组合键【ctrl+c】进入中断模式,由于我们是手动中断的,需要执行【~0s】命令将调试器上下文切换到托管线程上下文中。

1 0:009> ~0s
2 ntdll!NtWriteFile+0x14:
3 00007ffd`ece6d0e4 c3              ret

                    继续执行【!clrstack -a】命令,查看托管线程调用栈和所有参数和变量。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x1c20 (0)
 3         Child SP               IP Call Site
 4 0000003E0397E0E0 00007ffdece6d0e4 [InlinedCallFrame: 0000003e0397e0e0]
 5 0000003E0397E0E0 00007ffdc9b87d6b [InlinedCallFrame: 0000003e0397e0e0]
 6 。。。。。。(省略了)
 7 0000003E0397E800 00007FFCC6E51ABF ExampleCore_6_7.Program.Run()
 8     PARAMETERS:
 9         this (0x0000003E0397E870) = 0x0000020b49409628
10     LOCALS:
11         0x0000003E0397E858 = 0x000000000378734a
12 
13 0000003E0397E870 00007FFCC6E51988 ExampleCore_6_7.Program.Main(System.String[])
14     PARAMETERS:
15         args (0x0000003E0397E8B0) = 0x0000020b49408e90
16     LOCALS:
17         0x0000003E0397E898 = 0x0000020b49409628
18 
19 0:000>

                    0x0000020b49409628 这个就是 Program 对象地址,我们可以使用【!do 0x0000020b49409628】命令,确认一下。

1 0:000> !do 0x0000020b49409628
2 Name:        ExampleCore_6_7.Program
3 MethodTable: 00007ffcc6f00100
4 EEClass:     00007ffcc6eefb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None

                    证明了我们的猜想。我们知道对象的地址指向的是类型句柄,如果想要查看对象头的数据,还要减去 4 或者 8 个字节才是对象头的地址,4 或者 8 是根据系统的位数 32 位就减去 4,64 位就减去 8,从对象的地址也可以看出是该减去 8 还是 4,我的对象地址是 0x0000020b49409628,就要减去 8 了。
                    执行【dp 0x0000020b49409628-0x8 l1】命令,查看对象头的数据。

1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620  0f78734a`00000000

                    我们看到了对象头的值是 0f78734a,这个值是可以推出来的。我们知道对象的 HashCode 的值是 58225482,这个数字是十进制的结果值,我们转换成十六进制,看看是多少。

1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a

                    0378734a 这个值和【dp】命令的结果 0f78734a 类似,我们再使用 58225482 十六进制表示 0378734a,分别加上 0x080000000x04000000,执行命令【? 0378734a++0x08000000+0x04000000】,这个值就是对象头的值。

1 0:000> ? 0378734a++0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000`0f78734a

                    00000000`0f78734a 这个值和【dp】命令的输出是一样的,说明对象头保存是散列码了。
                    我们恢复调试器的执行,直到调试器输出“Press any key to release lock”字样,点击【ctrl+c】组合键,进入中断模式。
                    如图:
                    

                    由于 GC 会执行垃圾回收,内存压缩和对象地址转移,我们避免产生误操作。还是先执行线程切换【~0s】。

1 0:002> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3              ret

                    我们执行【!clrstack -a】命令查看托管线程调用栈,查找我们的Program 对象。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x1e14 (0)
 3         Child SP               IP Call Site
 4 000000EB377AE170 00007ff942b0d0a4 [InlinedCallFrame: 000000eb377ae170]
 5 000000EB377AE170 00007ff91b2076eb [InlinedCallFrame: 000000eb377ae170]
 6 。。。。。。(省略了)
 7 
 8 000000EB377AE4C0 00007FF85B971AEC ExampleCore_6_7.Program.Run()
 9     PARAMETERS:
10         this (0x000000EB377AE530) = 0x0000020c1a409628
11     LOCALS:
12         0x000000EB377AE518 = 0x000000000378734a
13 
14 000000EB377AE530 00007FF85B971988 ExampleCore_6_7.Program.Main(System.String[])
15     PARAMETERS:
16         args (0x000000EB377AE570) = 0x0000020c1a408e90
17     LOCALS:
18         0x000000EB377AE558 = 0x0000020c1a409628

                    0x0000020c1a409628 这个地址就是我们的 Program对象的地址,我们可以使用【!DumpObj 0x0000020c1a409628】命令确认一下。

1 0:000> !DumpObj 0x0000020c1a409628
2 Name:        ExampleCore_6_7.Program
3 MethodTable: 00007ff85ba20100
4 EEClass:     00007ff85ba0fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None

                    我们现在就可以查看对象头中的内容了。执行命令【dp 0x0000020c1a409628-0x8 l1】,由于我的程序是64位的,所以需要减去 8,32位减去4就可以了。

1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620  08000001`00000000

                    由于内容太多了,需要创建同步块存储内容,所以在对象头中就存储同步块的索引了。08000000 表示是同步块,1 表示同步块在同步块表中的索引位置。
                    此时,我们可以使用【!syncblk 0x1】命令查看同步块的信息了。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     1 0000024F99D534E8            1         1 0000020F030FE480 3e34   0   0000020f07809628 ExampleCore_6_7.Program
4 -----------------------------
5 Total           1
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch Excutable】,加载我们的项目文件 ExampleCore_6_7.exe,进入到调试器后,我们使用【g】命令直接运行调试器,直到控制台程序输出“Press any key to acquire lock”字样。我们回到调试器界面,点击【Break】按钮,进入中断模式,开始我们的调试旅程。
                    由于我们手动中断,所以必须切换到托管线程上下文中,因为当前在调试器的上下文环境中,执行命令【~0s】切换线程上下文。

1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ffd`ece6d0a4 c3              ret

                    继续执行【!clrstack -a】命令,查看托管线程调用栈和所有参数。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x1138 (0)
 3         Child SP               IP Call Site
 4 00000026DAD7E8A0 00007ffdece6d0a4 [InlinedCallFrame: 00000026dad7e8a0] 
 5 00000026DAD7E8A0 00007ffd667676eb [InlinedCallFrame: 00000026dad7e8a0] 
 6 。。。。。。(省略无用的)
 7 
 8 00000026DAD7EBF0 00007ffcc0fa1aa0 ExampleCore_6_7.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\Program.cs @ 17]
 9     PARAMETERS:
10         this (0x00000026DAD7EC60) = 0x000001c4c6409628
11     LOCALS:
12         0x00000026DAD7EC48 = 0x000000000378734a
13 
14 00000026DAD7EC60 00007ffcc0fa1988 ExampleCore_6_7.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\Program.cs @ 8]
15     PARAMETERS:
16         args (0x00000026DAD7ECA0) = 0x000001c4c6408e90
17     LOCALS:
18         0x00000026DAD7EC88 = 0x000001c4c6409628

                    红色标注的地址就是 0x000001c4c6409628 就是 Program 类型对象的地址,我们可以使用【!do 0x000001c4c6409628】命令验证。

1 0:000> !do 0x000001c4c6409628
2 Name:        ExampleCore_6_7.Program
3 MethodTable: 00007ffcc1050100
4 EEClass:     00007ffcc103fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None

                    继续使用【dp 0x000001c4c6409628-0x8 l1】命令,查看对象头的数据。

1 0:000> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620  0f78734a`00000000

                    对象头的当前值 0f78734a,表示在对象头中保存的是散列码,我们控制台程序散列码的输出值是 58225482,这个数字是十进制的,我们转换为十六进制,看看结果。

1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a

                    我们看到了十进制的 58225482 转换为十六进就是  0378734a,0x08000000 这个掩码只能确定是不是散列码,也有可能是同步块索引,只有在加上一个 0x04000000 掩码才能确定是散列码,所以,我们使用执行【? 00000000`0378734a+0x08000000+0x04000000】命令,这个结果就是对象头的值。

1 0:000> ? 00000000`0378734a+0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000`0f78734a

                    0f78734a 这个值和【dp】命令的输出是一样的,说明对象头保存是散列码了。
                    我们恢复调试器的执行,直到控制台程序输出“Press any key to release lock”字样,回到调试器,点击【Break】按钮,继续进入中断模式。如图:
                    

                    我们继续执行【dp 0x000001c4c6409628-0x8 l1】命令,看看对象头的输出。说明一下,在执行此命令之前,最好执行一次【!clrstack -a】命令获取对象地址,然后执行【!do】命令确认对象,最后在执行这个【dp】命令,因为垃圾收集器会在任意时刻移动对象,对象的地址也可能变化。

1 0:001> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620  08000001`00000000

                    08000001 这个结果值就很合理了,就是同步块索引了。此时,我们可以使用【!syncblk 0x1】命令查看同步块的信息了。

1 0:001> !syncblk 0x1
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     1 000001C4C1FC6268            1         1 000001C4C1F29FE0 1138   0   000001c4c6409628 ExampleCore_6_7.Program
4 -----------------------------
5 Total           1(同步块表中同步块的总数量)
6 CCW             0(COM 可调用包装的数量)
7 RCW             0(运行时可调用包装的数量)
8 ComClassFactory 0
9 Free            0(在同步块表中多少个同步块)


        4.3.3、瘦锁
            A、基础知识
                在 CLR 2.0 中引入了瘦锁,它实现了一种更高效的机制管理锁。在使用瘦锁时,保存在对象头中唯一的信息就是获取锁的线程 ID(既没有同步块),它是一个自旋锁(spinning lock)。因为要实现一个更为高效的等待锁,需要保存更多的信息。然后,这个瘦锁并不会无限的循环,而是当自旋到某个阈值就会停止。如果超过了这个阈值还不能获取这个锁,那么接下来就会创建一个实际的同步块,并将相应的信息保存下来来实现一个高效的等待(例如一个事件)。
                CLR 通常采用以下算法来判断是使用同步块和瘦锁。
                I、如果同步块存在,则使用同步块存储锁信息。
                II、如果同步块不存在,判断在当前对象的对象头中是否可以包含一个瘦锁。
                如果可以容纳,就将线程 ID 保存在对象头中。如果后面需要保存更多的信息,那么将自动创建一个同步块,并把当前对象头中的内容转移到新的同步块中。
                如果不可以容纳,就会创建一个新的同步块,并将对象头的内容转移到新的同步块中,并保存锁。
                我们可以通过调试器来验证这个算法,通过以下三步就可以了。
                1】、在获取锁之前,将同步块转储出来,验证其为空。
                2】、获取这个锁,中断程序执行,并验证已经创建了一个瘦锁。
                3】、获取散列码,中断程序执行,并验证这个瘦锁已经被一个同步块替代了。
            
                我们可以使用【!DumpHeap -thinlock】命令找出托管堆上所有带有瘦锁的对象。

            B、眼见为实
                调试源码:ExampleCore_6_8
                调试任务:验证瘦锁存储的算法。
                1)、NTSD 调试
                    编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.exe】打开调试器。
                    进入调试器,【g】直接运行,直到调试器输出,并暂停,如图:
                    

                    按【ctrl+c】组合键进入中断模式,还需要切换到托管线程上下文中,执行【~0s】命令,继续执行【!clrstack -a】命令查找 Program 对象。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x16d4 (0)
 3         Child SP               IP Call Site
 4 000000B881DDE628 00007ff942b0e814 [PrestubMethodFrame: 000000b881dde628] System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean)
 5 。。。。。。(省略了)
 6 000000B881DDE980 00007FF83A191A52 ExampleCore_6_8.Program.Run()
 7     PARAMETERS:
 8         this (0x000000B881DDEA00) = 0x000001c613c09628
 9     LOCALS:
10         0x000000B881DDE9E8 = 0x0000000000000000
11 
12 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
13     PARAMETERS:
14         args (0x000000B881DDEA40) = 0x000001c613c08e90
15     LOCALS:
16         0x000000B881DDEA28 = 0x000001c613c09628

                    0x000001c613c09628 就是 Program 类型对象地址,执行【!do 0x000001c613c09628】命令验证一下。

1 0:000> !do 0x000001c613c09628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass:     00007ff83a22fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None

                    执行【dp 0x000001c613c09628-8 l1】命令查看对象头的内容。

1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620  00000000`00000000

                    0 就是表示没有任何值。继续【g】恢复调试器的执行,直到调试器输出,如图:
                    

                    继续执行切换线程和查看线程的命令,分别是【~0s】、【!clrstack -a】查找我们的 Program 对象。

 1 0:001> ~0s
 2 ntdll!NtWriteFile+0x14:
 3 00007ff9`42b0d0e4 c3              ret
 4 
 5 0:000> !clrstack -a
 6 OS Thread Id: 0x16d4 (0)
 7         Child SP               IP Call Site
 8 000000B881DDE260 00007ff942b0d0e4 [InlinedCallFrame: 000000b881dde260]
 9 000000B881DDE260 00007ff91e0b7d6b [InlinedCallFrame: 000000b881dde260]
10 。。。。。。(省略了)
11 000000B881DDE980 00007FF83A191A71 ExampleCore_6_8.Program.Run()
12     PARAMETERS:
13         this (0x000000B881DDEA00) = 0x000001c613c09628
14     LOCALS:
15         0x000000B881DDE9E8 = 0x0000000000000000
16 
17 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
18     PARAMETERS:
19         args (0x000000B881DDEA40) = 0x000001c613c08e90
20     LOCALS:
21         0x000000B881DDEA28 = 0x000001c613c09628

                    继续执行【!do 0x000001c613c09628】命令,查看内容。

 1 0:000> !do 0x000001c613c09628
 2 Name:        ExampleCore_6_8.Program
 3 MethodTable: 00007ff83a240100
 4 EEClass:     00007ff83a22fb48
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
 8 Fields:
 9 None
10 ThinLock owner 1 (000001C60F9C8A80), Recursive 0

                    ThinLock owner 1 (000001C60F9C8A80), Recursive 0 说明对象上有了一个瘦锁,线程对象的 ID 是 000001C60F9C8A80,递归技术是 0。
                    继续执行【dp 0x000001c613c09628-8 l1】命令,查看对象头。

1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620  00000001`00000000

 

                    这里的 1 就是持有锁的线程 ID,是托管线程的 ID 值。可以使用【!t】或者【!threads】命令验证。

 1 0:000> !t
 2 ThreadCount:      2
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     16d4 000001C60F9C8A80    2a020 Preemptive  000001C613C13D60:000001C613C14660 000001C60F9C0540 -00001 MTA
11    6    2     2724 000001C60FA11810    21220 Preemptive  0000000000000000:0000000000000000 000001C60F9C0540 -00001 Ukn (Finalizer)

                    【dp】命令和【!t】命令都能找到 000001C60F9C8A80 这个指针的值。
                    我们继续【g】恢复调试器的执行,直到调试器输出如图:
                    

                    此时,说明对象的锁和散列值都保存了,然后我们【ctrl+c】进入中断模式,切换线程【~0s】,并且执行【!clrstack -a】命令查找 Program 对象,查一下它的状态。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x16d4 (0)
 3         Child SP               IP Call Site
 4 000000B881DDE630 00007ff942b0d0a4 [InlinedCallFrame: 000000b881dde630]
 5 000000B881DDE630 00007ff91e0b76eb [InlinedCallFrame: 000000b881dde630]
 6 。。。。。。(省略了)
 7 000000B881DDE980 00007FF83A191B0E ExampleCore_6_8.Program.Run()
 8     PARAMETERS:
 9         this (0x000000B881DDEA00) = 0x000001c613c09628
10     LOCALS:
11         0x000000B881DDE9E8 = 0x000000000378734a
12 
13 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
14     PARAMETERS:
15         args (0x000000B881DDEA40) = 0x000001c613c08e90
16     LOCALS:
17         0x000000B881DDEA28 = 0x000001c613c09628
18 
19 0:000>

                    执行【!do 0x000001c613c09628】命令,查看一下该对象有什么变化吗?

1 0:000> !do 0x000001c613c09628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass:     00007ff83a22fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None(这里没有东西了,锁信息已经转到同步块中保存了。)

                    继续执行【dp 0x000001c613c09628-8 l1】命令,查看一下对象头保存的数据。

1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620  08000001`00000000

                    08000001 看到这个值就知道是同步块索引了。我们使用【!syncblk】命令查看同步块的数据。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3 -----------------------------(这里是需要有值的,我这里没有输出,原因不知道,重来一次就可以)
4 Total           1
5 CCW             0
6 RCW             0
7 ComClassFactory 0
8 Free            0

                    我们也可以使用【!DumpHeap -thinlock】命令查找托管堆上所有具有瘦锁的对象。

1 0:000> !DumpHeap -thinlock
2          Address               MT     Size
3 000001c613c12ec0 00007ff83a295820       24 ThinLock owner 1 (000001C60F9C8A80) Recursive 0
4 Found 1 objects.

                    内容很简单,就不解释了。


                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】---【Launch executable】,加载我们的控制台项目 ExampleCore_6_8.exe,点击【打开】进入调试器。
                    进入调试器后,直接执行【g】命令,运行调试器,直到我们的控制台程序输出“Press any key to acquire lock”,此时,回到调试器,点击【Break】按钮,进入到中断模式,开始我们的调试。
                    由于我们是手动中断的,当前是调试器的上下文,需要切换到托管上下文中,需要执行【~0s】命令。

1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3              ret

                    我们使用【!clrstack -a】命令,查看托管线程调用栈,找出我们的 Program 类型的局部变量 program。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x34b8 (0)
 3         Child SP               IP Call Site
 4 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100] 
 5 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100] 
 6 。。。。。。(省略了)
 7 
 8 0000006CE77EE450 00007ff80e731a52 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 14]
 9     PARAMETERS:
10         this (0x0000006CE77EE4D0) = 0x000001ace3409628
11     LOCALS:
12         0x0000006CE77EE4B8 = 0x0000000000000000
13 
14 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
15     PARAMETERS:
16         args (0x0000006CE77EE510) = 0x000001ace3408e90
17     LOCALS:
18         0x0000006CE77EE4F8 = 0x000001ace3409628

                    0x000001ace3409628 就是Program 类型的实例对象的地址,我们可以使用【!do 0x000001ace3409628】来验证。

1 0:000> !do 0x000001ace3409628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass:     00007ff80e7cfb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None

                    我们执行命令【dp 0x000001ace3409628-8 l1】查看它的对象头。

1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620  00000000`00000000

                    00000000`00000000 表示没有任何数据。
                    我们【g】恢复调试器的执行,直到控制台程序输出“Press any key to get hashcode”,此时,对象已经获取了锁,但是还没有获取散列值。回调调试器中,点击【Break】按钮,再次进入中断模式,继续我们的调试。
                    由于手动进入中断模式,所以需要有调试器上下文切换到托管线程上下文中,执行命令【~0s】。

1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3              ret

                    继续执行【!clrstack -a】命令查找 Program 对象。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x34b8 (0)
 3         Child SP               IP Call Site
 4 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100] 
 5 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100] 
 6 。。。。。。(省略了)
 7 0000006CE77EE450 00007ff80e731a78 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 19]
 8     PARAMETERS:
 9         this (0x0000006CE77EE4D0) = 0x000001ace3409628
10     LOCALS:
11         0x0000006CE77EE4B8 = 0x0000000000000000
12 
13 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
14     PARAMETERS:
15         args (0x0000006CE77EE510) = 0x000001ace3408e90
16     LOCALS:
17         0x0000006CE77EE4F8 = 0x000001ace3409628

                    0x000001ace3409628 这就是我们的 Program 类型实例的地址,可以执行【!do 0x000001ace3409628】命令来验证,我就省略了。
                    此时,该对象已经获取锁了,我们查看对象头的数据,执行【dp 0x000001ace3409628-8 l1】命令。

1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620  00000001`00000000

                    00000001 这个就是所有者线程的 ID,此时我们可以执行【!do 0x000001ace3409628】或者【!DumpObj 0x000001ace3409628】命令,查看Program 对象,也有体现。

 1 0:000> !do 0x000001ace3409628
 2 Name:        ExampleCore_6_8.Program
 3 MethodTable: 00007ff80e7e0100
 4 EEClass:     00007ff80e7cfb48
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
 8 Fields:
 9 None
10 ThinLock owner 1 (000001ACDEFF2770), Recursive 0

                    红色标注的告诉我们 Program 对象上获取了一个瘦锁,线程对象指针是 000001ACDEFF2770 ,且递归计数位0,我们可以使用【!t】或者【!threads】命令来验证。

 1 0:000> !t
 2 ThreadCount:      2
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     34b8 000001ACDEFF2770    2a020 Preemptive  000001ACE3412F38:000001ACE3414660 000001acdf03e270 -00001 MTA 
11    5    2     419c 000001ACDF01B210    21220 Preemptive  0000000000000000:0000000000000000 000001acdf03e270 -00001 Ukn (Finalizer) 

                    我们看到了【!do】命令和【!t】命令的输出线程ID都是 000001ACDEFF2770,在对象头中包含了持有锁的线程 ID。
                    接下来,我们执行代码,获取散列码,再次中断执行,查看同步块和瘦锁的状态。
                    【g】继续运行,直到我们的控制台程序输出“HashCode:58225482 Press any key to release lock”。此时已经有了锁,并且也获取了散列码。回到调试器,点击【Break】按钮,进入中断模式,继续调试。
                    继续切换线程上下文【~0s】,并执行【!clrstack -a】命令查找我们的 Program 对象。

 1 0:001> ~0s
 2 ntdll!NtReadFile+0x14:
 3 00007ff9`42b0d0a4 c3              ret
 4 
 5 0:000> !clrstack -a
 6 OS Thread Id: 0x34b8 (0)
 7         Child SP               IP Call Site
 8 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100] 
 9 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100] 
10 。。。。。。(省略了)
11 0000006CE77EE450 00007ff80e731ae8 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 25]
12     PARAMETERS:
13         this (0x0000006CE77EE4D0) = 0x000001ace3409628
14     LOCALS:
15         0x0000006CE77EE4B8 = 0x000000000378734a
16 
17 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
18     PARAMETERS:
19         args (0x0000006CE77EE510) = 0x000001ace3408e90
20     LOCALS:
21         0x0000006CE77EE4F8 = 0x000001ace3409628

                    执行【!do 0x000001ace3409628】命令,查看 Program 对象。

1 0:000> !do 0x000001ace3409628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass:     00007ff80e7cfb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None(这里没有任何信息了,已经移到同步块中了)

                    继续执行【dp 0x000001ace3409628-8 l1】命令,查看对象头。

1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620  08000001`00000000

                    08000001 说明现在已经在使用同步块保存数据了,索引值是 1。
                    我们使用【!syncblk】命令来验证一下。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     1 000001ED75B257D8            1         1 000001ACDEFF2770 34b8   0   000001ace3409628 ExampleCore_6_8.Program
4 -----------------------------
5 Total           1
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    当然,我们可以使用【!DumpHeap -thinlock】命令找出托管堆上所有带有瘦锁的对象。

1 0:000> !DumpHeap -thinlock
2           Object           Thread               OSId      Recursion
3     01ace3412ec0     01acdeff2770             0x34b8              0

                    很简单,就不多说了。

    4.4、同步任务
        4.4.1、死锁
            A、基础知识
                死锁:当两个或者多个线程分别持有一些被保护的资源,并且都拒绝释放各自的资源而等待另一方释放资源时,死锁就产生了。
                这里会用到一些【k】命令,我就稍作介绍,【k】命令显示给定线程的堆栈帧以及相关信息,【kp】显示堆栈跟踪中调用的每个函数的所有参数。【kb】显示传递给堆栈跟踪中每个函数的前三个参数。
                如果想学更多的命令,可以去微软官网:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/k--kb--kc--kd--kp--kp--kv--display-stack-backtrace-

            B、眼见为实
                调试源码:ExampleCore_6_9
                调试任务:手动调试线程死锁的问题。
                1)、NTSD 调试
                    编译项目,然后直接运行我们的 EXE 可执行程序,直到我们的程序输出如图:
                    

                    此时,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD -pn ExampleCore_6_9.exe】通过进程名称附加我们的程序,当然,也可以通过进程 id 来附加我们的程序。
                    回车,直接进入调试器,调试器会有一个 int 3 的中断,就可以开始我们的调试了。
                    已经成功附加进程,截图效果,不是全部:
                    

                    此时,调试已经处于中断模式了,效果如图:
                    

                    我们可以使用【~*e!clrstack】命令,将托管线程和非托管线程的栈回溯都转储出来。

 1 0:007> ~*e!clrstack
 2 OS Thread Id: 0x2860 (0)
 3         Child SP               IP Call Site
 4 000000FBA677E2B0 00007ff8a9c8d0a4 [InlinedCallFrame: 000000fba677e2b0]
 5 000000FBA677E2B0 00007ff8961676eb [InlinedCallFrame: 000000fba677e2b0]
 6 000000FBA677E280 00007FF8961676EB Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
 7 000000FBA677E370 00007FF89616C9C0 System.ConsolePal+WindowsConsoleStream.ReadFileNative(IntPtr, System.Span`1<Byte>, Boolean, Int32 ByRef, Boolean)
 8 000000FBA677E3D0 00007FF89616C8BB System.ConsolePal+WindowsConsoleStream.Read(System.Span`1<Byte>)
 9 000000FBA677E410 00007FF89616FB84 System.IO.ConsoleStream.Read(Byte[], Int32, Int32)
10 000000FBA677E480 00007FFFE0CE89F1 System.IO.StreamReader.ReadBuffer()
11 000000FBA677E4D0 00007FFFE0CE90D4 System.IO.StreamReader.ReadLine()
12 000000FBA677E580 00007FF89617005D System.IO.SyncTextReader.ReadLine()
13 000000FBA677E5D0 00007FF896169319 System.Console.ReadLine()
14 000000FBA677E600 00007FFF81B71B08 ExampleCore_6_9.Program.Main(System.String[])
15 OS Thread Id: 0x2e20 (1)
16 Unable to walk the managed stack. The current thread is likely not a
17 managed thread. You can run !threads to get a list of managed threads in
18 the process
19 Failed to start stack walk: 80070057
20 OS Thread Id: 0x2a8c (2)
21 Unable to walk the managed stack. The current thread is likely not a
22 managed thread. You can run !threads to get a list of managed threads in
23 the process
24 Failed to start stack walk: 80070057
25 OS Thread Id: 0x3260 (3)
26         Child SP               IP Call Site
27 000000FBA707F9F0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000fba707f9f0]
28 OS Thread Id: 0x1d7c (4)4号托管线程的调用栈---》执行---》System.Threading.Monitor.ReliableEnter(说明在这里等待了)
29         Child SP               IP Call Site
30 000000FBA737F098 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba737f098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
31 000000FBA737F1F0 00007FFF81B724DE ExampleCore_6_9.Program+<>c.<Main>b__2_0()(NTSD 没有显示源码行号,Windbg Preview是有的,更容易调试)
32 000000FBA737F340 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
33 000000FBA737F390 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
34 000000FBA737F430 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
35 000000FBA737F4C0 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
36 000000FBA737F810 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba737f810]
37 OS Thread Id: 0x2444 (5)
38         Child SP               IP Call Site
39 000000FBA638F418 00007ff8a9c8db34 [HelperMethodFrame: 000000fba638f418] System.Threading.WaitHandle.WaitOneCore(IntPtr, Int32)
40 000000FBA638F520 00007FFFE0C00C04 System.Threading.WaitHandle.WaitOneNoCheck(Int32)
41 000000FBA638F580 00007FFFE0C18F66 System.Threading.PortableThreadPool+GateThread.GateThreadStart()
42 000000FBA638F910 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba638f910]
43 OS Thread Id: 0x1130 (6)(6号托管线程的调用栈)---》执行--》System.Threading.Monitor.ReliableEnter(说明在这里等待了,没有进入)
44         Child SP               IP Call Site
45 000000FBA74FF258 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba74ff258] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
46 000000FBA74FF3B0 00007FFF81B7215E ExampleCore_6_9.Program+<>c.<Main>b__2_1()(源码的调用位置,NTSD 没显示行号,Windbg Preview 是有行号的,更易调试)
47 000000FBA74FF500 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
48 000000FBA74FF550 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
49 000000FBA74FF5F0 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
50 000000FBA74FF680 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
51 000000FBA74FF9D0 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba74ff9d0]
52 OS Thread Id: 0x12d0 (7)
53 Unable to walk the managed stack. The current thread is likely not a
54 managed thread. You can run !threads to get a list of managed threads in
55 the process
56 Failed to start stack walk: 80070057

                    其实,我们从红色标注的可以看出一些端倪,OS Thread Id: 0x1d7c (4) 4号托管线程执行源码 ExampleCore_6_9.Program+<>c.<Main>b__2_0() 这个代码时,调用同步原语 Monitor 的 System.Threading.Monitor.ReliableEnter 方法想进入,却没进入,处于等待,因为后面没有调用栈了。说明一下,Windbg Preview 是可以显示源码行号的,可以直到在哪里处于等待,但是在 NTSD 是没有的。

                    OS Thread Id: 0x1130 (6) 的 6 号托管线程执行源码 ExampleCore_6_9.Program+<>c.<Main>b__2_1() 时调用了 System.Threading.Monitor.ReliableEnter 方法,想获取锁,由于后面没有执行,所以也是出于等待状态。
                    此时,我们知道他们都是处于等待状态,虽然输出的信息很简单,但是它却展示了一种常见的死锁识别技术。
                    这个输出的信息有点多,其实我们还可以使用另外一个命令,【!syncblk】查看同步快表的数据,也能看出一些信息。

 1 0:007> !syncblk
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     4 000002AAA66FC1D0            3         1 0000026A0FB1CF60 1130   6   0000026a14010a40 ExampleCore_6_9.Student
 4     5 000002AAA66FC228            3         1 0000026A114DE970 1d7c   4   0000026a14010a28 ExampleCore_6_9.Person
 5 -----------------------------
 6 Total           6
 7 CCW             0
 8 RCW             0
 9 ComClassFactory 0
10 Free            0

                    4 号托管线程持有 0000026a14010a28 ExampleCore_6_9.Person 对象,也就是锁定了该对象,我们的控制台程序输出也能说明这一点,输出是“tid=4,已经进入 Person(1111) 锁”,结合【~*e!clrstack】命令的输出,我们知道,4 号线程在执行 Monitor 的 Enter 方法的时候处于等待状态,我们就可以退出等待的位置在源码的 17 行,如图:
                    

                    再用同样的道理分析,6 号托管线程已经持有 0000026a14010a40 ExampleCore_6_9.Student 对象,说明该对象已经被锁定了,在结合【~*e!clrstack】命令的输出,我们知道 6 号线程在执行 Monitor 的 Enter 方法时是处于等待的状态,我们在结合我们控制台程序的输出“tid=6,已经进入 Student(22222) 锁”,我们可以知道源码在 32 行处于等待的。如图:

                    代码很简单,所以我们分析也不难。我们可以根据【~*e!clrstack】命令的输出,分别切换到 4 和 6 号线程上查看一下具体调用栈,也能找出问题。
                    我们先切换到 4 号线程,执行命令【~4s】。

1 0:007> ~4s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3              ret

                    我们继续执行【!clrstack -a】命令,查看一下调用栈的局部变量,主要观察 Person 和 Student 。

 1 0:004> !clrstack -a
 2 OS Thread Id: 0x1d7c (4)
 3         Child SP               IP Call Site
 4 000000FBA737F098 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba737f098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
 5 000000FBA737F1F0 00007FFF81B724DE ExampleCore_6_9.Program+<>c.<Main>b__2_0()
 6     PARAMETERS:
 7         this (0x000000FBA737F340) = 0x0000026a14009628
 8     LOCALS:
 9         0x000000FBA737F328 = 0x0000026a14010a28(这个就是我们的 ExampleCore_6_9.Person 对象)
10         0x000000FBA737F320 = 0x0000000000000001
11         0x000000FBA737F2F8 = 0x0000000000000000
12         0x000000FBA737F2F0 = 0x0000026a14010a40(这个就是我们的 ExampleCore_6_9.Student 对象)
13         0x000000FBA737F2E8 = 0x0000000000000000
14 
15 000000FBA737F340 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
16     PARAMETERS:
17         threadPoolThread (0x000000FBA737F390) = 0x0000026a1400aaa0
18         executionContext = <no data>
19         callback = <no data>
20         state = <no data>
21     LOCALS:
22         0x000000FBA737F368 = 0x0000000000000000
23         <no data>
24         <no data>
25         <no data>
26 
27 000000FBA737F390 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
28     PARAMETERS:
29         this (0x000000FBA737F430) = 0x0000026a14009698
30         currentTaskSlot (0x000000FBA737F438) = 0x0000026a1400c6c0
31         threadPoolThread = <no data>
32     LOCALS:
33         0x000000FBA737F3C8 = 0x0000000000000000
34         0x000000FBA737F3C0 = 0x0000026a140098d8
35         <no data>
36         0x000000FBA737F3F4 = 0x0000000000000000
37         <no data>
38         <no data>
39 
40 000000FBA737F430 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
41     LOCALS:
42         <CLR reg> = 0x0000026a14009bb0
43         <CLR reg> = 0x0000026a1400c6f8
44         <no data>
45         <CLR reg> = 0x0000026a1400c8e8
46         <CLR reg> = 0x0000026a1400aaa0
47         <CLR reg> = 0x00000000001b0116
48         <no data>
49         <no data>
50         <no data>
51         <no data>
52         <no data>
53 
54 000000FBA737F4C0 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
55     LOCALS:
56         <CLR reg> = 0x0000026a1400a688
57         <CLR reg> = 0x0000026a1400a908
58         <CLR reg> = 0x0000026a1400a9b0
59         <CLR reg> = 0x0000000000004e20
60         <no data>
61         <no data>
62         <no data>
63         <no data>
64 
65 000000FBA737F810 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba737f810]

                    0x0000026a14010a28 和 0x0000026a14010a40 就是我们的 ExampleCore_6_9.Person 对象和 ExampleCore_6_9.Student 对象,我们可以执行【!do 0x0000026a14010a28】和【!do 0x0000026a14010a40】命令来确认它们。

 1 0:004> !do 0x0000026a14010a28
 2 Name:        ExampleCore_6_9.Person
 3 MethodTable: 00007fff81c73300
 4 EEClass:     00007fff81c3c3e0
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
 8 Fields:
 9 None
10 
11 0:004> !do 0x0000026a14010a40
12 Name:        ExampleCore_6_9.Student
13 MethodTable: 00007fff81c73930
14 EEClass:     00007fff81c3c5f8
15 Tracked Type: false
16 Size:        24(0x18) bytes
17 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
18 Fields:
19 None

                    我们在分别查看一下这两个对象的对象头包含了什么数据,执行命令【dp 0x0000026a14010a28-8 l1】和【dp 0x0000026a14010a40-8 l1】。

1 0:004> dp 0x0000026a14010a28-8 l1
2 0000026a`14010a20  08000005`00000000
3 
4 0:004> dp 0x0000026a14010a40-8 l1
5 0000026a`14010a38  08000004`00000000

                    说明它们都使用了同步块保存数据和锁信息了。此时,可以再使用【!syncblk】命令查看同步块表的数据,上面已经执行,此处省略。
                    以下就简单了,根据我们的代码查找问题吧。
                    
                2)、Windbg Preview 调试
                    编译项目,然后直接运行我们的 EXE 可执行程序,我们的程序输出如图:
                    

                    然后,打开【Windbg Preview】,依次点击【文件】----【Attach to Process】,附加我们的进程,进入调试器,我们先把进程中所有线程转储出来看看,执行【~*e!clrstack】命令。

 1 0:007> ~*e!clrstack
 2 OS Thread Id: 0x35e8 (0)
 3         Child SP               IP Call Site
 4 00000035F5D7E7E0 00007ffeddc8d0a4 [InlinedCallFrame: 00000035f5d7e7e0] 
 5 00000035F5D7E7E0 00007ffe22d376eb [InlinedCallFrame: 00000035f5d7e7e0] 
 6 00000035F5D7E7B0 00007ffe22d376eb Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr) [/_/src/libraries/System.Console/src/Microsoft.Interop.LibraryImportGenerator/Microsoft.Interop.LibraryImportGenerator/LibraryImports.g.cs @ 412]
 7 00000035F5D7E8A0 00007ffe22d3c9c0 System.ConsolePal+WindowsConsoleStream.ReadFileNative(IntPtr, System.Span`1, Boolean, Int32 ByRef, Boolean) [/_/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @ 1150]
 8 00000035F5D7E900 00007ffe22d3c8bb System.ConsolePal+WindowsConsoleStream.Read(System.Span`1) [/_/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @ 1108]
 9 00000035F5D7E940 00007ffe22d3fb84 System.IO.ConsoleStream.Read(Byte[], Int32, Int32) [/_/src/libraries/System.Console/src/System/IO/ConsoleStream.cs @ 34]
10 00000035F5D7E9B0 00007ffdff8c89f1 System.IO.StreamReader.ReadBuffer() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 613]
11 00000035F5D7EA00 00007ffdff8c90d4 System.IO.StreamReader.ReadLine() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 802]
12 00000035F5D7EAB0 00007ffe22d4005d System.IO.SyncTextReader.ReadLine() [/_/src/libraries/System.Console/src/System/IO/SyncTextReader.cs @ 77]
13 00000035F5D7EB00 00007ffe22d39319 System.Console.ReadLine() [/_/src/libraries/System.Console/src/System/Console.cs @ 752]
14 00000035F5D7EB30 00007ffda0751b08 ExampleCore_6_9.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 41]
15 OS Thread Id: 0x3188 (1)
16 Unable to walk the managed stack. The current thread is likely not a 
17 managed thread. You can run !clrthreads to get a list of managed threads in
18 the process
19 Failed to start stack walk: 80070057
20 OS Thread Id: 0x40d4 (2)
21 Unable to walk the managed stack. The current thread is likely not a 
22 managed thread. You can run !clrthreads to get a list of managed threads in
23 the process
24 Failed to start stack walk: 80070057
25 OS Thread Id: 0x3bc4 (3)
26         Child SP               IP Call Site
27 00000035F64FFC50 00007ffeddc8db34 [DebuggerU2MCatchHandlerFrame: 00000035f64ffc50] 
28 OS Thread Id: 0x6c (4)
29         Child SP               IP Call Site
30 00000035F67FF098 00007ffeddc8db34 [HelperMethodFrame_1OBJ: 00000035f67ff098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
31 00000035F67FF1F0 00007ffda07528ee ExampleCore_6_9.Program+c.b__2_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 17]
32 00000035F67FF340 00007ffdff7e6532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 264]
33 00000035F67FF390 00007ffdff800698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2349]
34 00000035F67FF430 00007ffdff7ef430 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @ 913]
35 00000035F67FF4C0 00007ffdff7fc203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @ 102]
36 00000035F67FF810 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f67ff810] 
37 OS Thread Id: 0x2a40 (5)
38         Child SP               IP Call Site
39 00000035F598F1B8 00007ffeddc8db34 [HelperMethodFrame: 00000035f598f1b8] System.Threading.WaitHandle.WaitOneCore(IntPtr, Int32)
40 00000035F598F2C0 00007ffdff7e0c04 System.Threading.WaitHandle.WaitOneNoCheck(Int32) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs @ 128]
41 00000035F598F320 00007ffdff7f8f66 System.Threading.PortableThreadPool+GateThread.GateThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.GateThread.cs @ 48]
42 00000035F598F6B0 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f598f6b0] 
43 OS Thread Id: 0x3dd8 (6)
44         Child SP               IP Call Site
45 00000035F697EF68 00007ffeddc8db34 [HelperMethodFrame_1OBJ: 00000035f697ef68] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
46 00000035F697F0C0 00007ffda075256e ExampleCore_6_9.Program+c.b__2_1() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 32]
47 00000035F697F210 00007ffdff7e6532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 264]
48 00000035F697F260 00007ffdff800698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2349]
49 00000035F697F300 00007ffdff7ef430 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @ 913]
50 00000035F697F390 00007ffdff7fc203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @ 102]
51 00000035F697F6E0 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f697f6e0] 
52 OS Thread Id: 0x4344 (7)
53 Unable to walk the managed stack. The current thread is likely not a 
54 managed thread. You can run !clrthreads to get a list of managed threads in
55 the process
56 Failed to start stack walk: 80070057

                    【~*e!clrstack】命令将托管线程和非托管线程所有的栈回溯都输出出来了。OS Thread Id: 0x3dd8 (6) 号的线程执行 System.Threading.Monitor.ReliableEnter 方法就不执行了,说明卡住了,卡在什么地方呢,就是 ExampleCore_6_9.Program+c.b__2_1() 这样代码最后的行号,32,也就是源码的第32行,换句话说,就是 6 号线程持有 Student 锁,等待 Person 释放锁。效果如图:
                    

                    OS Thread Id: 0x6c (4) 号线程执行了 System.Threading.Monitor.ReliableEnter 方法也没有后续了,说明卡住了,同样,卡住的位置在哪里,就是 ExampleCore_6_9.Program+c.b__2_0() 这行表示的意思,最后有一个数字,就是源码的行号,它是17,换句话说,就是 4 号线程持有 Person 锁,在登台 student 上的锁释放。效果如图:
                    

                    其实,我们从以上也能看出一些端倪来。输出信息虽然简单,但是却展示一种常见死锁的识别技术。

                    我们也可以使用【!syncblk】命令查看一下同步块数据,这个也能说明一些问题。

 1 0:007> !syncblk
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     5 0000019606A7CF78            3         1 0000015570129FB0 6c   4   0000015574410a28 ExampleCore_6_9.Person
 4     6 0000019606A7CFD0            3         1 0000019606A77790 3dd8   6   0000015574410a40 ExampleCore_6_9.Student
 5 -----------------------------
 6 Total           6
 7 CCW             0
 8 RCW             0
 9 ComClassFactory 0
10 Free            0

                    我们看到了 ID 是 4 的线程持有 ExampleCore_6_9.Person 对象,ID 是 6 的线程持有 ExampleCore_6_9.Student 对象,我们可以切换到 4 和 6 号线程上查看一下。
                    通过以上的分析,剩下就去代码里找问题吧。


        4.4.2、孤立锁:异常
            A、基础知识
                孤儿锁是因为开发者使用 Monitor.Enter 获取一个对象后,因为某种原因没有正确调用 Monitor.Exit,导致这个对象一直处于占用状态,其他线程也就无法进入了,强烈建议使用 lock 语法。

            B、眼见为实
                调试源码:ExampleCore_6_10
                调试任务:重现孤立锁。
                1)、NTSD 调试
                    编译项目,直接双击我们项目的 EXE 可执行程序,直到我们的控制台程序有如图输出:
                    
                    我们打开【Visual Studio 2022 Developer Command Prompt v17.9.6】,输出命令【NTSD -pn ExampleCore_6_10.exe】,进入调试器,开始我们的调试了。
                    我们先执行【~*e!clrstack】命令,查看一下所有线程的调用栈是什么情况。

 1 0:004> ~*e!clrstack
 2 OS Thread Id: 0x29c (0)
 3         Child SP               IP Call Site
 4 000000F24A77E548 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000f24a77e548] System.Threading.Monitor.Enter(System.Object)
 5 000000F24A77E6A0 00007FFF844A1A6D ExampleCore_6_10.Program.Main(System.String[])
 6 OS Thread Id: 0x2128 (1)
 7 Unable to walk the managed stack. The current thread is likely not a
 8 managed thread. You can run !threads to get a list of managed threads in
 9 the process
10 Failed to start stack walk: 80070057
11 OS Thread Id: 0x3378 (2)
12 Unable to walk the managed stack. The current thread is likely not a
13 managed thread. You can run !threads to get a list of managed threads in
14 the process
15 Failed to start stack walk: 80070057
16 OS Thread Id: 0x16d0 (3)
17         Child SP               IP Call Site
18 000000F24AEFFBC0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000f24aeffbc0]
19 OS Thread Id: 0x3eb4 (4)
20 Unable to walk the managed stack. The current thread is likely not a
21 managed thread. You can run !threads to get a list of managed threads in
22 the process
23 Failed to start stack walk: 80070057
24 0:004>

                    OS Thread Id: 0x29c (0) 这个就是 0 号主线程,它执行了 Main 方法,又执行 System.Threading.Monitor.Enter 方法,处于挂起的状态,其他线程没有任何有用信息。
                    我们的被锁的对象是 ExampleCore_6_10.DBWrapper,又是在主线程出的问题,我们就去主线程上找一下 DBWrapper 对象。
                    执行命令【~0s】切换到主线程。

1 0:004> ~0s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3              ret

                    继续执行【!dumpstackobjects】命令。

 1 0:000> !dumpstackobjects
 2 OS Thread Id: 0x29c (0)
 3 RSP/REG          Object           Name
 4 000000F24A77E068 000002cf3c00e050 System.IO.StreamWriter
 5 000000F24A77E080 000002cf3c00e050 System.IO.StreamWriter
 6 000000F24A77E0C0 000002cf3c00e050 System.IO.StreamWriter
 7 000000F24A77E3B0 000002cf3c009630 ExampleCore_6_10.DBWrapper
 8 000000F24A77E450 000002cf3c009600 System.WeakReference`1[[System.Diagnostics.Tracing.EventSource, System.Private.CoreLib]]
 9 000000F24A77E4F8 000002cf3c009630 ExampleCore_6_10.DBWrapper
10 000000F24A77E558 000002cf3c00e050 System.IO.StreamWriter
11 000000F24A77E5D8 0000030fce2b04c0 System.String    Acquiring Lock!
12 000000F24A77E650 000002cf3c009630 ExampleCore_6_10.DBWrapper
13 000000F24A77E660 000002cf3c009630 ExampleCore_6_10.DBWrapper
14 000000F24A77E6B0 000002cf3c009688 System.Threading.Thread
15 000000F24A77E6C0 000002cf3c009648 System.Threading.ThreadStart
16 000000F24A77E6C8 000002cf3c009688 System.Threading.Thread
17 000000F24A77E6D0 000002cf3c009648 System.Threading.ThreadStart
18 000000F24A77E6E0 000002cf3c009630 ExampleCore_6_10.DBWrapper
19 000000F24A77E6E8 000002cf3c009688 System.Threading.Thread
20 000000F24A77E700 000002cf3c008e98 System.String[]
21 000000F24A77E7A8 000002cf3c008e98 System.String[]
22 000000F24A77E9A0 000002cf3c008e98 System.String[]
23 000000F24A77E9A8 000002cf3c008e98 System.String[]
24 000000F24A77EAC0 000002cf3c008e98 System.String[]
25 000000F24A77EB40 000002cf3c008eb0 System.String    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\bin\Debug\net8.0\ExampleCore_6_10.dll
26 000000F24A77EB50 000002cf3c008e98 System.String[]
27 000000F24A77EB60 000002cf3c008e78 System.String[]
28 000000F24A77EB98 000002cf3c008eb0 System.String    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\bin\Debug\net8.0\ExampleCore_6_10.dll
29 000000F24A77ED48 000002cf3c008e98 System.String[]
30 0:000>

                    ExampleCore_6_10.DBWrapper 类型的地址是 000002cf3c009630,执行【dp 000002cf3c009630-8 l1】命令查看一下它的对象头。

1 0:000> dp 000002cf3c009630-8 l1
2 000002cf`3c009628  08000002`00000000

                    说明对象头已经创建同步块了,索引值是 2,所以我们执行【!syncblk 2】命令查看一下同步块的数据。

 1 0:000> !syncblk 2
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     2 0000030FCE6B3F80            3         1 000002CF39578280 0 XXX   000002cf3c009630 ExampleCore_6_10.DBWrapper
 4 -----------------------------
 5 Total           2
 6 CCW             0
 7 RCW             0
 8 ComClassFactory 0
 9 Free            0
10 0:000>

                    说明 XXX 号线程持有 ExampleCore_6_10.DBWrapper 类型,也可以说 XXX 线程拥有  ExampleCore_6_10.DBWrapper 的锁。XXX 表示的是调试器线程的ID,0 表示操作系统线程的 ID。
                    XXX 的含义就是,CLR 无法将操作系统线程的 ID 映射到调试器线程,出现这样情况的一个原因是,某个线程在某个时刻获取一个对象的锁,然后,这个线程消失了,却没有释放锁。

                    我们可以执行【!t】或者【!threads】命令验证 XXX 的说法。

 1 0:000> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       1(有一个死亡的线程)
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      29c 000002CF37BB7940  202a020 Preemptive  000002CF3C009830:000002CF3C00A618 000002CF37BC5B20 -00001 MTA
11    3    2     16d0 000002CF37C82510    2b220 Preemptive  0000000000000000:0000000000000000 000002CF37BC5B20 -00001 MTA (Finalizer)
12 XXXX    4        0 000002CF39578280    39820 Preemptive  0000000000000000:0000000000000000 000002CF37BC5B20 -00001 Ukn(这个就是死亡的线程)

                    只要没有执行终结操作,即使处于死亡状态的线程也会被输出。
                    到这里就差不多了,我们还需要结合代码和调试器一起来找问题,很简单,我直接贴图了。
                    
                    图上说的很情况,就不多解释了。

                2)、Windbg Preview 调试
                    编译项目,直接双击我们项目的 EXE 可执行程序,直到我们的控制台程序有如图输出:
                    

                    我们打开【Windbg Preview】,依次点击【文件】---【Attach to process】,在右侧选择我们运行的程序,点击【附加】,附加我们的进程,进入调试器,开始我们的调试了。
                    我们先执行【~*e!clrstack】命令,查看一下所有线程的调用栈是什么情况。

 1 0:004> ~*e!clrstack
 2 OS Thread Id: 0x29c (0)
 3         Child SP               IP Call Site
 4 000000F24A77E548 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000f24a77e548] System.Threading.Monitor.Enter(System.Object)
 5 000000F24A77E6A0 00007fff844a1a6d ExampleCore_6_10.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\Program.cs @ 27]
 6 OS Thread Id: 0x2128 (1)
 7 Unable to walk the managed stack. The current thread is likely not a 
 8 managed thread. You can run !clrthreads to get a list of managed threads in
 9 the process
10 Failed to start stack walk: 80070057
11 OS Thread Id: 0x3378 (2)
12 Unable to walk the managed stack. The current thread is likely not a 
13 managed thread. You can run !clrthreads to get a list of managed threads in
14 the process
15 Failed to start stack walk: 80070057
16 OS Thread Id: 0x16d0 (3)
17         Child SP               IP Call Site
18 000000F24AEFFBC0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000f24aeffbc0] 
19 OS Thread Id: 0x12cc (4)
20 Unable to walk the managed stack. The current thread is likely not a 
21 managed thread. You can run !clrthreads to get a list of managed threads in
22 the process
23 Failed to start stack walk: 80070057

                    我们从命令的输出中可以看到,有用的信息不多,红色标注的就是主线程的运行情况。我们发现 0 号线程,也就是主线程在执行 System.Threading.Monitor.Enter 方法时挂起了,不执行了,问题大概也就是在这里。
                    既然主线程有了问题,我们就切换到主线程看看情况,执行命令【~0s】。

1 0:004> ~0s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3              ret

                    我们执行【!dumpstackobjects】命令,找到我们要分析的对象 DBWrapper。

 1 0:000> !dumpstackobjects
 2 OS Thread Id: 0x29c (0)
 3           SP/REG           Object Name
 4     00f24a77e068     02cf3c00e050 System.IO.StreamWriter
 5     00f24a77e080     02cf3c00e050 System.IO.StreamWriter
 6     00f24a77e0c0     02cf3c00e050 System.IO.StreamWriter
 7     00f24a77e3b0     02cf3c009630 ExampleCore_6_10.DBWrapper
 8     00f24a77e450     02cf3c009600 System.WeakReference<System.Diagnostics.Tracing.EventSource>
 9     00f24a77e4f8     02cf3c009630 ExampleCore_6_10.DBWrapper
10     00f24a77e558     02cf3c00e050 System.IO.StreamWriter
11     00f24a77e5d8     030fce2b04c0 System.String
12     00f24a77e650     02cf3c009630 ExampleCore_6_10.DBWrapper
13     00f24a77e660     02cf3c009630 ExampleCore_6_10.DBWrapper
14     00f24a77e6b0     02cf3c009688 System.Threading.Thread
15     00f24a77e6c0     02cf3c009648 System.Threading.ThreadStart
16     00f24a77e6c8     02cf3c009688 System.Threading.Thread
17     00f24a77e6d0     02cf3c009648 System.Threading.ThreadStart
18     00f24a77e6e0     02cf3c009630 ExampleCore_6_10.DBWrapper
19     00f24a77e6e8     02cf3c009688 System.Threading.Thread
20     00f24a77e700     02cf3c008e98 System.String[]
21     00f24a77e7a8     02cf3c008e98 System.String[]
22     00f24a77e9a0     02cf3c008e98 System.String[]
23     00f24a77e9a8     02cf3c008e98 System.String[]
24     00f24a77eac0     02cf3c008e98 System.String[]
25     00f24a77eb40     02cf3c008eb0 System.String
26     00f24a77eb50     02cf3c008e98 System.String[]
27     00f24a77eb60     02cf3c008e78 System.String[]
28     00f24a77eb98     02cf3c008eb0 System.String
29     00f24a77ed48     02cf3c008e98 System.String[]

                    ExampleCore_6_10.DBWrapper 就是我们要找的对象,它的地址是 02cf3c009630,我们执行【dp 02cf3c009630-8 l1】命令查看该对象的对象头包含的是什么东西。

1 0:000> dp 02cf3c009630-8 l1
2 000002cf`3c009628  08000002`00000000

                    08000002 说明对象头已经创建了一个同步块了,索引值是 2,我们查看同步块,执行命令【!syncblk 2】。

1 0:000> !syncblk 2
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     2 0000030FCE6B3F80            3         1 000002CF39578280 0 XXX   000002cf3c009630 ExampleCore_6_10.DBWrapper
4 -----------------------------
5 Total           2
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    输出信息告诉我们 ExampleCore_6_10.DBWrapper 对象已经被锁定了,被 XXX 线程锁定的。XXX 表示的是调试器的线程 ID,0 表示的是操作系统线程的 ID。
                    XXX 表示 CLR 无法将操作系统线程的 ID 无法映射到调试器线程。出现这种情况的原因是,这个线程在某个时刻获取了该对象上的锁,然后这个线程消失了但是却没有释放锁。

                    我们执行【!t】或者【!threads】命令验证这一点。

 1 0:000> !threads
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       1(有一个死亡的线程)
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      29c 000002CF37BB7940  202a020 Preemptive  000002CF3C009830:000002CF3C00A618 000002cf37bc5b20 -00001 MTA 
11    3    2     16d0 000002CF37C82510    2b220 Preemptive  0000000000000000:0000000000000000 000002cf37bc5b20 -00001 MTA (Finalizer) 
12 XXXX    4        0 000002CF39578280    39820 Preemptive  0000000000000000:0000000000000000 000002cf37bc5b20 -00001 Ukn (死亡的线程)

                    只要没有执行终结操作,即使处于死亡状态的线程也会被出输出。

                    要分析具体是哪里的错误,肯定要结合代码来分析。我们的代码是这里出问题了,如图:
                    

                    代码很简单,就不多说了。
                    
        4.4.3、线程中止
            这节的内容就略过了,探索的意义不是很大,首先,我使用的平台是 8.0 跨平台版本,不是 .NET Framework 版本了,如果在 .NET 8.0 版本里调用  Thread.Abort() 方法是不支持的。会有绿色波浪线提示,如图:
            

            我自己也在 Net framework 4.8 平台上测试,程序并没有出现停滞,是正常结束的,说明 Thread 的方法实现已经有所改变,原文内容写的有些过旧了。如果大家使用的 .NET Framework 平台,可以自己试试。测试源码:ExampleCore_6_10.ThreadAbort.Net


        4.4.4、终结器挂起
            系统内存暴涨有很多原因,不良线程可以是原因之一,访问非托管资源也可以是原因之一。如果查看内容暴涨,其实还是有很多方法的,比如:我们可以使用【任务管理器】,也可以使用【ProcessExplorer】工具。具体的使用方法就不介绍了,大家可以网上自行恶补。
            原书上的内容我省略了,由于没有原书的源码,所以我也无法调试了。这里是我用的以前的代码(我之前写过一个系列的代码),和终结器挂起也没关系,但是和内存暴涨有关系,原书的调试方法还是可以使用的,特此说明。

            有些查找问题的方法和步骤还是很有用的,如果我们发现系统内存暴涨,可以尝试执行一下步骤排查。
            1)、我们可以先执行【!eeheap -loader】命令,查看一下加载器堆是否存在异常。
            2)、如果加载器堆没问题,我们可以尝试执行【!eeheap -gc】命令查看托管堆是否有什么情况。
            3)、我们也可以执行【!heap -s】命令,查看所有堆的统计情况,来查找问题,如果数据有问题,可以继续使用【!heap -h】命令是否存在句柄数据。
            4)、当然,我们也可以使用【!DumpHeap -stat】命令,统计一下托管堆上的对象,看看对象数据是否存在问题。
            5)、直到了对象,我们就可以使用【!DumpHeap -type】查找指定对象的地址。
            6)、有了对象的地址,我们就可以使用【!gcroot】命令,观察对象的根引用。
            7)、我们也可以使用【FinalizeQueue】命令查看一下中介对象的情况来查找问题。
            8)、通过【!t】或者【!thread】命令,了解线程的情况,直到了线程标识 ID,我们就可以使用【!clrstack】命令查看 指定线程的调用栈。

              
五、总结
    这篇文章的终于写完了,这篇文章的内容相对来说,不是很多。写完一篇,就说明进步了一点点。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

posted on 2024-05-14 10:26  可均可可  阅读(542)  评论(1编辑  收藏  举报