使用WinDbg —— .NET篇 (九)

来源:互联网 发布:种族歧视知乎 编辑:程序博客网 时间:2024/05/21 09:21

7.3 Monitor (lock) ReaderWriterLockSlim

另外在.NET中,常用的锁通常是Monitor(也就是关键词lock)ReaderWriterLockSlim,其中Monitor的使用更加广泛,在本节也主要是介绍Monitor。在讲解Monitor之前,需要了解一下C#中甚至CLR中引用类型的内存结构,举个例子:

classLocker

{

   publicint 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”命令可以打印出MonitorHeld0的索引项,当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

classProgram

{

   staticobject resource =newobject();

   staticvoid Main(string[] args)

    {

       lock (resource)

        {

           Thread t =newThread(M1);

            t.Start();

            t.Join();

        }

       Console.WriteLine("Main: Never hit here.");

    }

 

   staticvoid M1()

    {

       lock (resource)

        {

           Console.WriteLine("M1: Never hit here.");

        }

    }

}

 

Infinite loop

classProgram

{

   staticobject locker1 =newobject();

   staticvoid Main(string[] args)

    {

       newThread(M1).Start();

       Thread.Sleep(100);

       lock (locker1)

        {

        }

 

       Console.WriteLine("Never hit here");

    }

 

   staticvoid M1()

    {

       lock (locker1)

        {

           while (true)

               Thread.Sleep(1000);

        }

    }

}

 

Transfer Task to UI

publicpartialclassForm1 :Form

{

   public Form1()

    {

        InitializeComponent();

    }

 

   privatevoid button1_Click(object sender, EventArgs e)

    {

       Thread t =newThread(Start);

        t.Start();

    }

 

   privatevoid Start()

    {

       lock (locker)

        {

           MessageBox.Show("Started the thread");

           this.Invoke(newAction(M1));

           MessageBox.Show("Never hit here");

        }

    }

 

   privateobject locker =newobject();

   privatevoid M1()

    {

       lock (locker)

        {

           MessageBox.Show("Never hit here.");

        }

    }

}

 

像以上列出来的场景可以通过分析锁的占用情况和线程的调用堆栈信息可以找到原因,也就是说可以通过“!syncblk, “!threads”, “~{0}s”, “!clrstack”等命令的灵活运用,找出多线程导致的Hang/Freeze等问题。

最后值得一提的是ReaderWriterLockSlim,这个类大体是为了实现“作者-读者”的多线程模式,也就是说多个读者可以同时工作;同一时间只有一个作者可以工作,其他的作者或读者都不能工作。简单的说就是读者与读者之间不冲突,读者与作者之间冲突,作者与作者之间冲突。ReaderWriterLockerSlim还支持一种叫做UpgradeableRead模式,UpgradeableRead与读者之间不冲突,但UpgradeableReadUpgradeableRead之间冲突,UpgradeableRead与作者之间冲突,而且这个类是通过管理EventWaitHandle来控制各个角色的冲突问题,具体用法不讲。

分析ReaderWriterLockSlim的方法与前面讲到的MutexSemaphoreEventWaitHandle类似,所以不在重复,在这里我打印出某个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锁或作者锁,在上面的信息来看线程的ManagedID5的线程占用了UpgradeRead锁,我们可以通过“!threads”命令找到ManagedID5的线程:

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的详细信息,前面有提到,这里就不细说了。

 

0 0
原创粉丝点击