GCHandle:何时明确使用GCHandleType.Normal?

时间:2014-08-12 21:45:38

标签: c# garbage-collection

阅读里希特J书的“手动监视和控制对象的生命周期”一节。 Jeffrey说,有两种方法可以使用GCHandle类控制对象的生命周期:

  • 使用GCHandleType.Normal调用Alloc方法(即使可能有来自应用程序代码的 no 引用,GC也无法删除objs)
  • 使用GCHandleType.Pinned调用Alloc方法(除了Normal,GC无法移动此类对象)

他说,两种方式都可以用来将托管对象传递给非托管代码。他试图解释,开发人员应该使用GCHandleType.Normal标志调用Alloc。我真的不明白正常标志使用的解释。在这两种方式中,我们都不允许GC收集对象,这些对象在GC描述符表中有这样的标记,但是在Pinned的情况下,我们还防止在垃圾收集期间移动这些对象。据我所知,在普通模式的情况下,不是直接引用(内存地址)传递给非托管代码,而只是传递给GC-descriptors表的索引。而当非托管代码回调托管代码时,则该索引将转换为当前/实际地址。我脑子里乱七八糟,谷歌和微软几乎没有细节信息,只能复制粘贴。

我的问题:

  1. 某些应用程序root(非弱)引用托管堆中的对象,而不再引用根。这是否意味着,GC-descriptors表中的相应条目将使用GCHandleType.Normal标志?看起来没有,因为杰弗里说,“即使可能没有来自应用程序代码的没有引用,GC也无法删除obj”。但如果不是,这个表条目会有哪个标志?再一次,MyClass mc = new MyClass(),在GC-descriptors表中为mc做的相应条目是否有Normal标志,如果没有,那么?
  2. 何时(以及如何,请短代码)开发人员真的需要使用GCHandleType.Normal标志?固定对我来说更清楚。

3 个答案:

答案 0 :(得分:4)

如果在使用GCHandleType.Normal创建句柄之前将对象引用传递给本机代码是不安全的,那么在创建这样的句柄之后它将是不安全的,因为非托管代码需要稳定的指针。因此,GCHandleType.Normal句柄对非托管代码没有任何作用。我认为这是一个文档错误,建议不然。

托管代码使用

GCHandleType.Normal来创建不会死的对象。例如,某些Timer类使自己的实例保持活动状态,以便在删除最后一个引用时计时器不会停止。

  

据我所知,在普通模式下,不是直接参考   (内存地址)传递给非托管代码,但只是索引   GC描述符表。

这不可能是真的,因为在发生PInvoke时,没有足够的信息来判断GCHandle是否与您想要传递的对象相关联。即使它想要,马萨勒也无法做到这一点。另外,非托管代码对句柄表条目有什么作用?它不明白。句柄表是CLR内部。

  

某些应用程序root(非弱)引用托管中的对象   堆,没有更多的根。这是否意味着,相应的条目   GC-descriptors表将使用GCHandleType.Normal标志?容貌   不,因为杰弗里说,“GC甚至不能删除objs   可能是没有来自应用程序代码的引用“。但如果没有,   这个表条目会有哪些标志?

它具有您在创建GCHandle时传入的标志。该表中只有条目GCHandle。不跟踪普通对象。

答案 1 :(得分:3)

来自GCHandle的文档:

  
    

提供从非托管内存访问托管对象的方法。

  

如果您打算从非托管代码访问该对象,则需要将该对象固定。为了获得固定对象,必须可以将其编组为非托管内存。

如果你只需要一个不透明的句柄传递给非托管代码,以便非托管代码可以再次传回它而不访问它,那么你需要一个固定对象,但你仍然需要确保它不被垃圾收集器删除。

考虑这个课程:

public class MyClass
{
    DateTime dt = DateTime.Now;
}

如果你试图像这样得到一个固定的句柄:

MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Pinned);

您将收到以下消息的异常:

  

对象包含非原始数据或非blittable数据。

这是因为返回的句柄允许您获取固定对象的地址。为了从非托管代码中使用此地址,必须将对象从托管内存编织到非托管内存。

此代码不会引发异常:

MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Normal);

因为您无法使用返回的句柄来获取地址。

所以回答你的问题:

  1. 一个manged对象(MyClass mc = new MyClass())在GC-descriptors表中没有条目。如果没有来自托管代码的引用,它将被垃圾收集(我认为必须是Jeffrey Richter所称的应用程序代码。我还没有读过书)。
  2. 当我需要将不透明句柄传递给非托管代码时,我使用GCHandleType.Normal。
  3. 一种情况是托管程序集的纯C API。 API可能如下所示:

    MYHANDLE h1 = MyLib_CreateComponent();
    MYHANDLE h2 = MyLib_CreateComponent();
    
    MyLib_SetX(h1, 9.81);
    double y1 = MYLib_CalcY(h1);
    MyLib_SetX(h2, 3.14);
    double y2 = MyLib_CalcY(h2);
    
    printf("z = %f\n", y1 + y2);
    
    MyLib_DestroyComponent(h1);
    MyLib_DestroyComponent(h2); 
    

    无法从C代码直接访问该对象。

    函数MyLib_CreateComponent()的C#实现如下所示:

     public static int CreateComponent()
     {
         MyClass instance = new MyClass();
         GCHandle gch = GCHandle.Alloc(instance, GCHandleType.Normal);
         IntPtr ip = GCHandle.ToIntPtr(h);
         h = ip.ToInt32();
         return h;
     }
    

    在托管代码中,我会创建一个方法来使用句柄获取对象:

    static MyClass GetObjectFromHandle(int hComp)
    {
        IntPtr ip = new IntPtr(hComp);
        GCHandle h = GCHandle.FromIntPtr(ip);
        MyClass comp = h.Target as MyClass;
        return comp;
    }
    

答案 2 :(得分:2)

根据经验,当您希望从本机代码(未访问)中传递指针时,可以使用Normal。并且,当您期望从本机代码访问指针时,请使用Pinned

这是因为当您将Normal对象传递给本机代码时,您只能将IntPtr传递给GCHandle(通过GCHandle.ToIntPt进行检索)。而且只能通过GCHandle.FromIntPt将其重新恢复到托管代码中。

可以在以下位置找到有关其工作原理的很好的解释:https://blogs.msdn.microsoft.com/jmstall/2006/10/09/gchandle-tointptr-vs-gchandle-addrofpinnedobject/