一個lock鎖就可以分出低中高水平的程序員對問題的處置方式
說到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# 這樣類似聖經級著作,而且對相關概念也比較清楚。
1. 清楚『引用類型』 在堆上的布局結構及棧上的指針是指向方法表索引(類型對象指針),如下圖。
2. 清楚當lock住對象後,它的『同步塊索引』 和 CLR上的『同步塊數組』是呈現一個關聯關係,然後又是一張圖。
牛X點: 僅僅用了兩張圖就把這個事情解決的相當完美,讀者一看就明白了,然來是每個線程在lock的時候會查看一下對象的同步塊索引所映射的同步塊數組中的坑中信息來判斷是否可以加鎖。
不足點: 一定要挑刺的話,那就是這類人只是在聽別人講故事,到底是不是真的如此其實自己心裏也沒譜,只是一味的相信對方的人格魅力,而真正🐮👃的人,十句話中只有一句假話~😄😄😄
第三類人
這類人就會動用資源或者人脈親自嘗試一下是不是如第二類人所描述的那樣,操刀的話,最好的工具就是windbg,接下來我就操刀一把。
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> 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> 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> 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#說的不是那麼一致,如果我是對的,那就是重大發現,如果是錯的,那就是水平有限😄😄😄,開個玩笑,可能新版本在底層做了進一步優化吧。
好了,本篇就說到這裡,希望對你有幫助