GCHandle:何时显式使用GCHandleType.Normal?

tri*_*kbz 5 c# garbage-collection

阅读Richter J书的“手动监视和控制对象的生存期”部分。Jeffrey说,有两种方法可以使用GCHandle类控制对象的生存期:

  • 使用GCHandleType.Normal调用Alloc方法(即使应用程序代码中可能没有引用,GC也无法删除obj )
  • 使用GCHandleType.Pinned调用Alloc方法(除“普通”之外,GC无法移动此类对象)

他说,两种方式都可以用来将托管对象传递给非托管代码。他试图解释,开发人员何时应使用GCHandleType.Normal标志调用Alloc。我不太了解有关正常标志用法的解释。在这两种方式中,我们都不允许GC收集对象,这些对象在GC描述符表中具有此类标志,但是在“固定”的情况下,我们还防止此类对象在垃圾收集期间移动。据我了解,在普通模式下,不是直接引用(内存地址)传递给非托管代码,而是传递给GC描述符表的索引。而且,当非托管代码被调用回托管代码时,该索引将被转换为当前/实际地址。我一头雾水,谷歌和微软几乎没有详细信息,只有复制粘贴。

我的问题:

  1. 一些应用程序根(不是弱根)引用托管堆中的对象,并且不再有根。这是否意味着GC描述符表中的相应条目将带有GCHandleType.Normal标志?由于Jeffrey说,看起来好像没有,“即使应用程序代码中可能没有引用,GC也无法删除obj ”。但是,如果否,此表项将具有哪个标志?同样,MyClass mc = new MyClass(),GC描述符表中mc的相应条目是否具有Normal标志,如果没有,那么该使用哪个?
  2. 什么时候(以及如何,请短代码)开发人员真正需要使用GCHandleType.Normal标志?固定对我来说更清楚。

Nil*_*nde 5

GCHandle的文档中:

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

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

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

考虑这个类:

public class MyClass
{
    DateTime dt = DateTime.Now;
}
Run Code Online (Sandbox Code Playgroud)

如果您尝试像这样获得固定句柄:

MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Pinned);
Run Code Online (Sandbox Code Playgroud)

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

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

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

此代码不会引发异常:

MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Normal);
Run Code Online (Sandbox Code Playgroud)

因为不能使用返回的句柄来获取地址。

所以回答你的问题:

  1. 托管对象 (MyClass mc = new MyClass()) 在 GC 描述符表中没有条目。当托管代码没有引用它时,它将被垃圾收集(我认为这一定是 Jeffrey Richter 所说的应用程序代码。我没有读过这本书)。
  2. 当我需要将不透明句柄传递给非托管代码时,我使用 GCHandleType.Normal 。

一种方案是用于托管程序集的纯 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); 
Run Code Online (Sandbox Code Playgroud)

不能从 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;
 }
Run Code Online (Sandbox Code Playgroud)

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

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


usr*_*usr 4

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

GCHandleType.Normal被托管代码用来创建不会消亡的对象。例如,某些Timer类使它们自己的实例保持活动状态,以便当您删除对它的最后一个引用时计时器不会停止。

据我了解,在正常模式下,不会将直接引用(内存地址)传递给非托管代码,而只是传递 GC 描述符表中的索引。

这不可能是真的,因为在 PInvoke 发生时,没有足够的信息来判断 a 是否GCHandle与您想要传递的对象关联。即使粉碎者想要这样做,也做不到。另外,非托管代码会对句柄表条目做什么?它不明白。句柄表是 CLR 内部的。

某些应用程序根(非弱)引用托管堆中的对象,并且不再有根。这是否意味着 GC 描述符表中的相应条目将带有 GCHandleType.Normal 标志?看起来不行,因为 Jeffrey 说,“即使应用程序代码中可能没有引用,GC 也无法删除对象”。但如果不是,该表条目将具有哪个标志?

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

  • _另外,非托管代码会对句柄表条目做什么?_ 我将尝试解释 Jeffrey Richter 在他的示例中的含义:假设您有一个“委托 void(对象状态)”,并且您希望本机代码给您回电通过这个委托使用一些传递给本机代码的状态对象。在这种情况下,您可以使用 Normal 选项创建“GCHandle”对象并将其(转换为 IntPtr)传递给本机代码。本机代码不会对句柄执行任何操作,它只是将句柄作为回调参数传递。这允许您从本机代码获取相同的状态对象,而无需限制 GC 固定它。 (2认同)