• 揭示同步块索引(中):如何获得对象的HashCode 转载


    揭示同步块索引(中):如何获得对象的HashCode

    题外话:为了尝鲜,也兴冲冲的安装了Win7,不过兴奋之余却郁闷不已,由于是用Live Writer写博客,写了好几篇草稿,都完成了80%左右,没有备份全部没了。欲哭无泪,只好重写了。

    Visual Studio + SOS 小实验

    咋一看标题,觉得有些奇怪,同步块索引和HashCode有啥关系呢。从名字上来看离着十万八千里。在不知道细节之前,我也是这样想的,知道细节之后,才发现这两兄弟如此亲密。我们还是先来用Visual Studio + SOS,看一个东西,下面是作为小白兔的示例代码:

       1: using System;
       2: public class Program
       3: {
       4:     static void Main()
       5:     {
       6:         Foo f = new Foo();
       7:         Console.WriteLine(f.GetHashCode());
       8:  
       9:         Console.ReadLine();
      10:     }
      11: }
      12: //就这么一个简单的类
      13: public class Foo
      14: {
      15:  
      16: }

    (使用Visual Studio + SOS调试的时候,请先在项目的属性,调试栏里设置“允许非托管代码调试”)

    我们分别在第7行,第9行设置断点,F5运行,当程序停在第一个断点处时(此时f.GetHashCode()还没有执行),我们在Visual Studio的立即窗口里输入:

       1: .load sos.dll
       2: extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
       3: !dso
       4: PDB symbol for mscorwks.dll not loaded
       5: OS Thread Id: 0x1730 (5936)
       6: ESP/REG  Object   Name
       7: 0013ed78 01b72d58 Foo
       8: 0013ed7c 01b72d58 Foo
       9: 0013efc0 01b72d58 Foo
      10: 0013efc4 01b72d58 Foo

    使用.load sos.dll加载sos模块,然后使用!dso,我们找到了Foo类型的f对象的内存地址:01b72d58,然后使用Visual Studio调试菜单下的查看内存的窗口,查看f对象头部的内容:

    image

    阴影遮住的00 00 00 00就是同步块索引所在的地方了,可以看得出来,此时同步块索引的值还是0(后面会对这个做解释),然后继续F5,程序运行到下一个断点处,这个时候f.GetHashCode()也已调用了,细心的你就会发现,原来对象同步块索引所在的地方的值变了:

    image

    Visual Studio这个内存查看器有个很好的功能,对内存变化的以红色标出。我们看到,原来是00 00 00 00变成了现在的4a 73 78 0f。嗯,看来HashCode的获取和同步块索引还是有一些关系的,不然调用GetHashCode方法为什么同步块索引的值会变化呢。再来看看Console.WriteLine(f.GetHashCode())的输出:

    image 
    不知道着两个值有没有什么关系,我们先把它们都换算成二进制吧。注意,这里的4a 73 78 0f是低位在左,高位在右,下面的十进制是高位再左,低位在右,那4a 73 78 0f实际上就是0x0f78734a了。

    0x0f78734a:00001111011110000111001101001010

       58225482:00000011011110000111001101001010

     Rotor源代码

    我们先用0补齐32位,突然发现这两者低26位居然是一模一样的(红色标出的部分),这是巧合还是必然?为了一探究竟只好搬出Rotor的源代码,从源代码里看看是否能发现什么东西。还是遵循老路子,我们先从托管代码开始:

       1: public virtual int GetHashCode()
       2: {
       3:    return InternalGetHashCode(this);
       4: }
       5: [MethodImpl(MethodImplOptions.InternalCall)]
       6: internal static extern int InternalGetHashCode(object obj);

    在本系列的第一篇文章已经提到过,标记有[MethodImpl(MethodImplOptions.InternalCall)]特性的方法是使用Native Code的方式实现的,在Rotor中,这些代码位于sscli20\clr\src\vm\ecall.cpp文件中:

       1: FCFuncElement("InternalGetHashCode", ObjectNative::GetHashCode)
       2: FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {
       3:     DWORD idx = 0;
       4:     OBJECTREF objRef(obj);
       5:     idx = GetHashCodeEx(OBJECTREFToObject(objRef));
       6:     return idx;
       7: }
       8: FCIMPLEND
       9: INT32 ObjectNative::GetHashCodeEx(Object *objRef)
      10: {
      11:     // This loop exists because we're inspecting the header dword of the object
      12:     // and it may change under us because of races with other threads.
      13:     // On top of that, it may have the spin lock bit set, in which case we're
      14:     // not supposed to change it.
      15:     // In all of these case, we need to retry the operation.
      16:     DWORD iter = 0;
      17:     while (true)
      18:     {
      19:         DWORD bits = objRef->GetHeader()->GetBits();
      20:  
      21:         if (bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
      22:         {
      23:             if (bits & BIT_SBLK_IS_HASHCODE)
      24:             {
      25:                 // Common case: the object already has a hash code
      26:                 return  bits & MASK_HASHCODE;
      27:             }
      28:             else
      29:             {
      30:                 // We have a sync block index. This means if we already have a hash code,
      31:                 // it is in the sync block, otherwise we generate a new one and store it there
      32:                 SyncBlock *psb = objRef->GetSyncBlock();
      33:                 DWORD hashCode = psb->GetHashCode();
      34:                 if (hashCode != 0)
      35:                     return  hashCode;
      36:  
      37:                 hashCode = Object::ComputeHashCode();
      38:  
      39:                 return psb->SetHashCode(hashCode);
      40:             }
      41:         }
      42:         else
      43:         {
      44:             // If a thread is holding the thin lock or an appdomain index is set, we need a syncblock
      45:             if ((bits & (SBLK_MASK_LOCK_THREADID | (SBLK_MASK_APPDOMAININDEX << SBLK_APPDOMAIN_SHIFT))) != 0)
      46:             {
      47:                 objRef->GetSyncBlock();
      48:                 // No need to replicate the above code dealing with sync blocks
      49:                 // here - in the next iteration of the loop, we'll realize
      50:                 // we have a syncblock, and we'll do the right thing.
      51:             }
      52:             else
      53:             {
      54:                 DWORD hashCode = Object::ComputeHashCode();
      55:  
      56:                 DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
      57:  
      58:                 if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
      59:                     return hashCode;
      60:                 // Header changed under us - let's restart this whole thing.
      61:             }
      62:         }
      63:     }
      64: }

    代码很多,不过大部分操作都是在做与、或、移位等。而操作的对象就是这行代码获取的:objRef->GetHeader()->GetBits(),实际上就是获取同步块索引。

    想想,在第一个断点命中的时候,同步块索引的值还是0x00000000,那应该是下面这块代码执行:

       1: DWORD hashCode = Object::ComputeHashCode();
       2: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
       3: if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
       4:     return hashCode;

    通过Object的ComputeHashCode方法算出一个哈希值来(由于本文不是关注哈希算法的,所以这里不讨论这个ComputeHashCode方法的实现)。然后进行几个或操作(这里还要与原先的bits或操作是为了保留原来的值,说明这个同步块索引还起了别的作用,比如上篇文章的lock),然后将同步块索引中老的位换掉。从这里我们还看不出来什么。不过,如果我们再次对这个对象调用GetHashCode()方法呢?那同步块索引不再为0x00000000,而是0x0f78734a,在来看看几个定义的常量的值:

       1: #define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX    0x08000000
       2: #define BIT_SBLK_IS_HASHCODE            0x04000000
       3: #define HASHCODE_BITS                   26
       4: #define MASK_HASHCODE                   ((1<<HASHCODE_BITS)-1)

    从刚才设置hashcode的地方可以看到:DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;

    所以开头的两个if都可以通过了,返回的hashcode就是bits & MASK_HASHCODE。

    这个MASK_HASHCODE是将1向左移26位=100000000000000000000000000,然后减1=00000011111111111111111111111111(低26位全部为1,高6位为0),然后与同步块索引相与,其实这里的作用不就是为了取出同步块索引的低26位的值么。再回想一下本文开头的那个试验,原来不是巧合啊。

    连上上一篇,我们可以看到同步块索引不仅仅起到lock的作用,有时还承担着存储HashCode的责任。实际上同步块索引是这样的一个结构:总共32位,高6位作为控制位,后26的具体含义随着高6位的不同而变化,高6位就像很多小开关,有的打开(1),有的关闭(0),不同位的打开和关闭有着不同的意义,程序也就知道低26位到底是干啥的了。这里的设计真是巧妙,不断占用内存很紧凑,程序也可以灵活处理,灵活扩展。

    后记

    本篇和上一篇一样,都是单独将独立的内容拿出来,这样可以更简单的来阐述。比如在本文中,我只设想同步块索引做hashcode的存储,这个时候,同步块索引就干干净净(本文前面的试验中先得到的同步块索引就是一个0),但实际中同步块索引可能担任更多的职责,比如既lock,又要获取HashCode,这个时候情况就更复杂,这个在后面一篇文章会综合各种情况更详细的说明。

  • 相关阅读:
    非阻塞式NIO 小案例(模拟聊天室)
    网络通信小案例,服务端接收成功要给客户端一个反馈(阻塞式)
    阻塞式网络通信小案例:
    NIO的非阻塞式网络通信
    字符编码
    使用分散(Scatter)与聚集(Gather)来实现文件的复制
    使用通道之间的数据传输(效果,也是实现文件的复制)
    创建直接缓存区完成文件的复制
    C++预处理详解
    C++的学习资源
  • 原文地址:https://www.cnblogs.com/qianyz/p/2224927.html
Copyright © 2020-2023  润新知