说到lock锁,我相信在座的各位没有不会用的,并且还知道怎么用不会出错,但让他们聊一聊为何能够锁住,都说人以群分,大概就有了下面低中高水平的三类人吧。程序员
将lock对象定义成static,这样就能让多个线程看到同一个对象,以此实现线程间互斥和保证同步,若是再深问为何?就怕遮遮掩掩的说好像每一个实例都有一个同步块索引,再展开的话就顶不住了,反正你们都这么写,我也不敢问,我也不会说,若是上代码,只能这样丢给你。数组
public class Program { public static object lockMe = new object(); public static void Main(string[] args) { var task1 = Task.Factory.StartNew(() => { lock (lockMe) { //todo } }); var task2 = Task.Factory.StartNew(() => { lock (lockMe) { //todo } }); Task.WaitAll(task1, task2); } }
这类人可能看过CLR via C# 这样相似圣经级著做,并且对相关概念也比较清楚。ide
1. 清楚‘引用类型’ 在堆上的布局结构及栈上的指针是指向方法表索引(类型对象指针),以下图。工具
2. 清楚当lock住对象后,它的‘同步块索引’ 和 CLR上的‘同步块数组’是呈现一个关联关系,而后又是一张图。布局
牛X点:仅仅用了两张图就把这个事情解决的至关完美,读者一看就明白了,然来是每一个线程在lock的时候会查看一下对象的同步块索引所映射的同步块数组中的坑中信息来判断是否能够加锁。测试
不足点:必定要挑刺的话,那就是这类人只是在听别人讲故事,究竟是不是真的如此其实本身内心也没谱,只是一味的相信对方的人格魅力,而真正的人,十句话中只有一句假话优化
这类人就会动用资源或者人脉亲自尝试一下是否是如第二类人所描述的那样,操刀的话,最好的工具就是windbg,接下来我就操刀一把。spa
1. 对‘引用类型’布局结构的补充线程
如今你们也知道了每一个对象都有两个额外开销,就是‘同步块索引’ + '方法表索引',在x86系统中,每一个索引各占4字节,而在x64系统中,每一个索引各占8字节,因个人系统是x64,按照x64版本测试。指针
2. 案例代码
有了上面的知识补充,接下来我开两个task,在task中进行lock操做。
namespace ConsoleApp2 { public class Program { public static void Main(string[] args) { var employee = new Employee(); Console.WriteLine("步骤一:lock前!!!"); Console.ReadLine(); var task1 = Task.Factory.StartNew(() => { lock (employee) { Console.WriteLine("步骤二:lock1中。。。。"); Console.ReadLine(); } Console.WriteLine("步骤二:退出lock1..."); }); var task2 = Task.Factory.StartNew(() => { lock (employee) { Console.WriteLine("步骤二:lock2中。。。。"); Console.ReadLine(); } Console.WriteLine("步骤二:退出lock2..."); }); Task.WaitAll(task1, task2); Console.WriteLine("步骤三:lock后,所有退出!"); Console.ReadLine(); } } public class Employee { public int a = 1; public int b = 2; } }
3. 使用windbg调试
我准备分三步骤实现,lock前,lock中,lock后,而后拿到这三种状况下的dump文件来展现 employee 对象的同步块索引 和 CLR全局同步块数组实时状况。
<1 style="box-sizing: border-box;"> lock前
先把程序跑起来,再从任务管理器中生成dump文件。
!threads -> ~0s -> !clrstack -l 这三个命令是为了寻找主线程栈上的局部变量 employee 的内存地址。
0:000> !threads ThreadCount: 2 UnstartedThread: 0 BackgroundThread: 1 PendingThread: 0 DeadThread: 0 Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 40b8 00000235222457f0 2a020 Preemptive 0000023523F76D00:0000023523F77FD0 000002352223b0f0 1 MTA 6 2 44c8 00000235222705f0 2b220 Preemptive 0000000000000000:0000000000000000 000002352223b0f0 0 MTA (Finalizer) 0:000> ~0s ntdll!ZwReadFile+0x14: 00007ffa`bd7baa64 c3 ret 0:000> !clrstack -l OS Thread Id: 0x40b8 (0) Child SP IP Call Site 0000005f721fe748 00007ffabd7baa64 [InlinedCallFrame: 0000005f721fe748] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 0000005f721fe748 00007ffaa5d7b7e8 [InlinedCallFrame: 0000005f721fe748] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 0000005f721fe710 00007ffaa5d7b7e8 *** ERROR: Module load completed but symbols could not be loaded for mscorlib.ni.dll DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 0000005f721fe7f0 00007ffaa65920cc System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef) LOCALS: <no data> <no data> <no data> <no data> <no data> <no data> 0000005f721fe880 00007ffaa6591fd5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) LOCALS: <no data> <no data> 0000005f721fe8e0 00007ffaa5d470f4 System.IO.StreamReader.ReadBuffer() LOCALS: <no data> <no data> 0000005f721fe930 00007ffaa5d47593 System.IO.StreamReader.ReadLine() LOCALS: <no data> <no data> <no data> <no data> 0000005f721fe990 00007ffaa6738b0d System.IO.TextReader+SyncTextReader.ReadLine() 0000005f721fe9f0 00007ffaa6530d98 System.Console.ReadLine() 0000005f721fea20 00007ffa485d0931 *** WARNING: Unable to verify checksum for ConsoleApp2.exe ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 19] LOCALS: 0x0000005f721feaa8 = 0x0000023523f72dc0 0x0000005f721feaa0 = 0x0000000000000000 0x0000005f721fea98 = 0x0000000000000000 0000005f721fecb8 00007ffaa7af6c93 [GCFrame: 0000005f721fecb8]
从最后的LOCALS中能够看到,当前主线程有三个局部变量,依次是:employee,task1,task2,而其中的 0x0000023523f72dc0 就是employee。
!dumpobj 0x0000023523f72dc0 -> !dumpobj 0000023523f72dd8 找到 employee 在堆上的内存区域
0:000> !dumpobj 0x0000023523f72dc0 Name: ConsoleApp2.Program+<>c__DisplayClass0_0 MethodTable: 00007ffa484c5af8 EEClass: 00007ffa484c2600 Size: 24(0x18) bytes File: C:\dream\Csharp\ConsoleApp1\ConsoleApp2\bin\x64\Debug\ConsoleApp2.exe Fields: MT Field Offset Type VT Attr Value Name 00007ffa484c5bb8 4000003 8 ConsoleApp2.Employee 0 instance 0000023523f72dd8 employee 0:000> !dumpobj 0000023523f72dd8 Name: ConsoleApp2.Employee MethodTable: 00007ffa484c5bb8 EEClass: 00007ffa484c2678 Size: 24(0x18) bytes File: C:\dream\Csharp\ConsoleApp1\ConsoleApp2\bin\x64\Debug\ConsoleApp2.exe Fields: MT Field Offset Type VT Attr Value Name 00007ffaa57685a0 4000001 8 System.Int32 1 instance 1 a 00007ffaa57685a0 4000002 c System.Int32 1 instance 2 b
使用菜单 view -> memory 查看 0000023523f72dd8 在堆上的布局,从图上看找的没有错哈。
00000235`23f72dc8 d8 2d f7 23 35 02 00 00 00 00 00 00 00 00 00 00 .-.#5........... 00000235`23f72dd8 b8 5b 4c 48 fa 7f 00 00 01 00 00 00 02 00 00 00 .[LH............
从上面看到,00000235`23f72dd8行的前8个字节就是employee的同步块索引,此时所有是0,好的,记录一下这个状态。
<2 style="box-sizing: border-box;"> lock中
继续在控制台按Enter,从图中能够看到lock1获取到了锁。
使用view -> memory 查看 0000023523f72dd8 内存索引地址,能够看到由原来的全0变成了 0000000007000008,以下图。
而后用 !syncblk -all 把CLR的全局同步块数组调出来,看看是否是占了一个坑位。
0:006> !syncblk -all Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 1 00000235222af108 0 0 0000000000000000 none 0000023523f77150 System.__ComObject 2 00000235222af158 0 0 0000000000000000 none 0000023523f77170 System.EventHandler`1[[Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs, mscorlib]] 3 00000235222af1a8 0 0 0000000000000000 none 0000023523f771b0 Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs 4 00000235222af1f8 0 0 0000000000000000 none 0000023523f79458 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback 5 00000235222af248 0 0 0000000000000000 none 0000023523f7a158 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback 6 00000235222af298 0 0 0000000000000000 none 0000023523f7a2f8 System.Object 7 00000235222af2e8 3 1 00000235222cb320 56a8 6 0000023523f72dd8 ConsoleApp2.Employee ----------------------------- Total 7 CCW 1 RCW 2 ComClassFactory 0 Free 0
看到最后一行了没?ConsoleApp2.Employee 占用的坑位编号是7,说明 0000000007000008 和这个 7 作了关联,同时MonitorHeld=3也说明当前有一个持有线程(+1),有一个等待线程(+2),因此这个观点也获得了验证。
<3 style="box-sizing: border-box;"> lock后
继续在控制台Enter,从图中能够看到两个lock都已经结束了。看此时employee会怎样?
而后仍是同样查看 0000023523f72dd8 的内存布局状况。
不过奇怪的是对象的同步块索引并无变,继续查看同步块数组。
0:000> !syncblk -all Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 1 00000235222af108 0 0 0000000000000000 none 0000023523f77150 System.__ComObject 2 00000235222af158 0 0 0000000000000000 none 0000023523f77170 System.EventHandler`1[[Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs, mscorlib]] 3 00000235222af1a8 0 0 0000000000000000 none 0000023523f771b0 Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs 4 00000235222af1f8 0 0 0000000000000000 none 0000023523f79458 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback 5 00000235222af248 0 0 0000000000000000 none 0000023523f7a158 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback 6 00000235222af298 0 0 0000000000000000 none 0000023523f7a2f8 System.Object 7 00000235222af2e8 0 0 0000000000000000 none 0000023523f72dd8 ConsoleApp2.Employee 8 00000235222af338 0 0 0000000000000000 none 0000023523f76750 System.IO.TextWriter+SyncTextWriter ----------------------------- Total 8 CCW 1 RCW 2 ComClassFactory 0 Free 0
从各项都是0来看,它已经处于初始化状态了,MonitorHeld=0也表示当前无线程持有ConsoleApp2.Employee,关于对象同步块索引没有变以及数组中的坑位,可能会被CLR后期惰性删除和初始化吧,谁知道呢?
貌似跟踪下来和CLR via C#说的不是那么一致,若是我是对的,那就是重大发现,若是是错的,那就是水平有限,开个玩笑,可能新版本在底层作了进一步优化吧。