7.3 Monitor (lock) 与 ReaderWriterLockSlim
另外在.NET中,常用的锁通常是Monitor(也就是关键词lock)和ReaderWriterLockSlim,其中Monitor的使用更加广泛,在本节也主要是介绍Monitor。在讲解Monitor之前,需要了解一下C#中甚至CLR中引用类型的内存结构,举个例子:
class Locker { public int i = 0; } |
如上表的Locker类型,在初始化的后内存分布如图:
从上图可以看到对象的引用地址首先指向的是TypeHandle(其实也就是MethodTable的地址),然后才是Locker实例对象的字段域。在这里值得注意的是对象地址的前4个字节的内存值为该对象的同步块索引(syncblk),同步块索引的值有两种含义,一种把四个字节分为两部分解读,高6位为控制位,低26位表明值,这值可以表示锁,哈希值或com组件信息,如图:
控制位的值标识着低26位的值是什么类型的值,例如当高6位为“000011”时,低26位的值为哈希值,当高位为0时,低26位的值标识该对象有没有没线程占有,如果有,那么这个值就为1,这时候这种锁被称为thinlock,通过sos中的“!dumpheap-thinlock”可以直接打出所有的这种锁:
0:006> !dumpheap -thinlock Address MT Size 02532f7c 00bd4da0 12 ThinLock owner 1 (00c5df60) Recursive 0 0:006> dd 02532f7c-4 l1 02532f78 00000001 0:006> !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 1e28 00c5df60 202a020 Preemptive 02532F94:00000000 00c57e00 1 MTA 5 2 1784 00c9a638 2b220 Preemptive 00000000:00000000 00c57e00 0 MTA (Finalizer) |
上表首先通过“!dumpheap-thinlock”打印出所有的thinlock,只有一个而且这个锁被线程为00c5df60占有,然后查看了一下这个索引块的值,为1;最后通过“!threads”命令找出这个占有thinlock的线程为0号线程。
当一个对象同时含有锁信息和哈希值信息或者其他的信息,这个时候同步块索引怎么储存这种值?这个时候同步块索引中低26位就不是对应的值了,而是同步块表(Sync Blocks Table)的索引值,同步块表中的每个条项含有对应对象额外的信息,包括锁,哈希值,com组件信息等,这时候这种锁就不是Thinlock了,就叫锁(lock)。通过sos中的“!syncblk-all”命令可以打印出同步块表中各个条项的信息:
0:007> !syncblk -all Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 1 00c934f4 0 0 00000000 none 024c2fa8 System.Threading.Thread 2 00c93528 3 1 00c71110 1e84 0 024c2f7c TestLock.Locker ----------------------------- Total 2 CCW 0 RCW 0 ComClassFactory 0 Free 0 |
在分析锁导致的问题时候,一般直接使用”!syncblk”命令打印出所有已经被线程占住的锁:
0:007> !syncblk Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 2 00c93528 3 1 00c71110 1e84 0 024c2f7c TestLock.Locker ----------------------------- Total 2 CCW 0 RCW 0 ComClassFactory 0 Free 0 |
第一列Index表明该锁的在索引表中的索;SyncBlock这一列不清楚什么意思,不用管他;MonitorHeld这一列的值比较有意思,这个值的计算方式是:当有线程已经获得这个锁了,那么这个值+1,如果有线程在等待这个锁,那么这个值+2,所以这个MonitorHeld的值要么为一个奇数要么为0,为0的时候表示没有任何线程进入这个锁,一般通过“!syncblk-all”命令可以打印出MonitorHeld为0的索引项,当MonitorHeld值为奇数的时候,表明有线程已经获得这个锁了,而且还能知道等待这个锁释放的线程数量,就是(MonitorHeld-1)/ 2;第四列是RecursionOwning表示已经获得了这个锁的线程进入该锁的次数,一般都为1,如果写了如下代码:
lock (obj) { ... lock(obj) { //... } } |
这个值就为2;然后是Owning Thread Info列下面有三个值:00c71110 1e84 0,这三个值都是拥有该锁的线程信息,第一个值对应ThreadOBJ的值,第二个值对应OSID,第三个值就是Windbg工具维护的线程的ID值(因为这个”sybcblk”命令打印出来的格式已经乱掉了,所以看这个打印信息的时候注意看每个值的相对位置);最后一列表明这个同步块索引项对应的是哪个对象,这一列有两个值:024c2f7cTestLock.Locker, 第一个值是该对象的地址,第二个值是该对象的类型。
通过这打印出来的信息,我们是无法直接知道死锁的位置,所以一般需要配合着看对应的线程调用栈和锁对应的实例的详细信息。
在SOSEX里面有可以直接检索死锁的命令:”!dlk”:
0:007> !dlk Examining SyncBlocks... Scanning for ReaderWriterLock(Slim) instances... Scanning for holders of ReaderWriterLock locks... Scanning for holders of ReaderWriterLockSlim locks... Examining CriticalSections... Scanning for threads waiting on SyncBlocks... Scanning for threads waiting on ReaderWriterLock locks... Scanning for threads waiting on ReaderWriterLocksSlim locks... Scanning for threads waiting on CriticalSections... *DEADLOCK DETECTED* CLR thread 0x1 holds the lock on SyncBlock 006f33b8 OBJ:024c2f7c[TestLock.Locker] ...and is waiting for the lock on SyncBlock 006f33ec OBJ:024c2f88[TestLock.Locker] CLR thread 0x3 holds the lock on SyncBlock 006f33ec OBJ:024c2f88[TestLock.Locker] ...and is waiting for the lock on SyncBlock 006f33b8 OBJ:024c2f7c[TestLock.Locker] CLR Thread 0x1 is waiting at System.Threading.Monitor.Enter(System.Object, Boolean ByRef)(+0x17 Native) CLR Thread 0x3 is waiting at System.Threading.Monitor.Enter(System.Object, Boolean ByRef)(+0x17 Native)
1 deadlock detected. |
但是这个命令的检索有一定的局限性,也就是这个命令只能检索出死锁的问题,像很多其他的Hang或者说Freeze的问题不一定是死锁导致的,这一类问题就很不能被“!dlk”找出来。下面我列举一些常见的Hang/Freeze的场景(不包括死锁):
Thread Join |
class Program { static object resource = new object(); static void Main(string[] args) { lock (resource) { Thread t = new Thread(M1); t.Start(); t.Join(); } Console.WriteLine("Main: Never hit here."); }
static void M1() { lock (resource) { Console.WriteLine("M1: Never hit here."); } } } |
Infinite loop |
class Program { static object locker1 = new object(); static void Main(string[] args) { new Thread(M1).Start(); Thread.Sleep(100); lock (locker1) { }
Console.WriteLine("Never hit here"); }
static void M1() { lock (locker1) { while (true) Thread.Sleep(1000); } } } |
Transfer Task to UI |
public partial class Form1 : Form { public Form1() { InitializeComponent(); }
private void button1_Click(object sender, EventArgs e) { Thread t = new Thread(Start); t.Start(); }
private void Start() { lock (locker) { MessageBox.Show("Started the thread"); this.Invoke(new Action(M1)); MessageBox.Show("Never hit here"); } }
private object locker = new object(); private void M1() { lock (locker) { MessageBox.Show("Never hit here."); } } } |
像以上列出来的场景可以通过分析锁的占用情况和线程的调用堆栈信息可以找到原因,也就是说可以通过“!syncblk”, “!threads”, “~{0}s”, “!clrstack”等命令的灵活运用,找出多线程导致的Hang/Freeze等问题。
最后值得一提的是ReaderWriterLockSlim,这个类大体是为了实现“作者-读者”的多线程模式,也就是说多个读者可以同时工作;同一时间只有一个作者可以工作,其他的作者或读者都不能工作。简单的说就是读者与读者之间不冲突,读者与作者之间冲突,作者与作者之间冲突。 ReaderWriterLockerSlim还支持一种叫做UpgradeableRead模式,UpgradeableRead与读者之间不冲突,但UpgradeableRead与UpgradeableRead之间冲突,UpgradeableRead与作者之间冲突,而且这个类是通过管理EventWaitHandle来控制各个角色的冲突问题,具体用法不讲。
分析ReaderWriterLockSlim的方法与前面讲到的Mutex,Semaphore,EventWaitHandle类似,所以不在重复,在这里我打印出某个ReaderWriterLockSlim的详细信息:
0:011> !do 0249307c Name: System.Threading.ReaderWriterLockSlim MethodTable: 59babf0c EEClass: 599e0a60 Size: 68(0x44) bytes File: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll Fields: MT Field Offset Type VT Attr Value Name 5c078998 400019b 3c System.Boolean 1 instance 0 fIsReentrant 5c0807a0 400019c 1c System.Int32 1 instance 0 myLock 5c07b3ac 40001a0 20 System.UInt32 1 instance 0 numWriteWaiters 5c07b3ac 40001a1 24 System.UInt32 1 instance 0 numReadWaiters 5c07b3ac 40001a2 28 System.UInt32 1 instance 0 numWriteUpgradeWaiters 5c07b3ac 40001a3 2c System.UInt32 1 instance 1 numUpgradeWaiters 5c078998 40001a4 3d System.Boolean 1 instance 0 fNoWaiters 5c0807a0 40001a5 30 System.Int32 1 instance 5 upgradeLockOwnerId 5c0807a0 40001a6 34 System.Int32 1 instance -1 writeLockOwnerId 5c07c49c 40001a7 c ...g.EventWaitHandle 0 instance 00000000 writeEvent 5c07c49c 40001a8 10 ...g.EventWaitHandle 0 instance 00000000 readEvent 5c07c49c 40001a9 14 ...g.EventWaitHandle 0 instance 0249a014 upgradeEvent 5c07c49c 40001aa 18 ...g.EventWaitHandle 0 instance 00000000 waitUpgradeEvent 5c078a7c 40001ac 4 System.Int64 1 instance 1 lockID 5c078998 40001ae 3e System.Boolean 1 instance 0 fUpgradeThreadHoldingRead 5c07b3ac 40001b0 38 System.UInt32 1 instance 4 owners 5c078998 40001b6 3f System.Boolean 1 instance 0 fDisposed 5c078a7c 40001ab 3f4 System.Int64 1 static 1 s_nextLockID 59bacc9c 40001ad 0 ...ReaderWriterCount 0 TLstatic t_rwc >> Thread:Value << |
从打印出来的ReaderWriterLockSlim的实例信息的来看,这里面包含了足够多关于锁的详细信息:等待的Reader锁,等待的Writer锁,哪个线程占用了UpgradeRead锁或作者锁,在上面的信息来看线程的ManagedID为5的线程占用了UpgradeRead锁,我们可以通过“!threads”命令找到ManagedID为5的线程:
0:011> !threads ThreadCount: 11 UnstartedThread: 4 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 1f3c 00cfb280 2a020 Preemptive 0249F2C0:00000000 00cc3478 1 MTA 5 2 2a34 00d07938 2b220 Preemptive 00000000:00000000 00cc3478 0 MTA (Finalizer) 6 3 1038 00d1f998 202b020 Preemptive 02494028:00000000 00cc3478 0 MTA XXXX 4 0 00d26dc0 1400 Preemptive 00000000:00000000 00cc3478 0 Ukn 8 5 2da4 00d27438 202b020 Preemptive 02498008:00000000 00cc3478 0 MTA 7 6 2950 00d27d98 202b020 Preemptive 02496028:00000000 00cc3478 0 MTA XXXX 7 0 00d286f8 1400 Preemptive 00000000:00000000 00cc3478 0 Ukn 9 8 750 00d29058 202b020 Preemptive 0249A058:00000000 00cc3478 0 MTA 10 9 2b80 00d299b8 202b020 Preemptive 0249C028:00000000 00cc3478 0 MTA XXXX 10 0 00d2a318 1400 Preemptive 00000000:00000000 00cc3478 0 Ukn XXXX 11 0 00d2ac78 1400 Preemptive 00000000:00000000 00cc3478 0 Ukn |
然后通过查看8号线程的调用栈信息就能知道具体的占用该锁的逻辑了。同时在ReaderWriterLockSlim的实例信息里面还包括具体占用锁的EventWaitHandle的信息,关于怎么查看EventWaitHandle的详细信息,前面有提到,这里就不细说了。