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

8

阅读 Richter J 的书中 "Monitoring and Controlling the Lifetime of Objects Manually" 部分。Jeffrey说,使用 GCHandle 类有两种方法可以“控制”对象的生命周期:

  • 使用 GCHandleType.Normal 调用 Alloc 方法(即使应用程序代码中可能没有对对象的引用,GC也不能删除对象)
  • 使用 GCHandleType.Pinned 调用 Alloc 方法(除了 Normal 模式,还可以防止 GC 移动这种对象)

他说,两种方法都可以用于将托管对象传递到非托管代码中。他试图解释开发人员何时应该使用带有 GCHandleType.Normal 标志的 Alloc。我并不真正理解有关 Normal 标志使用的解释。在两种方式中,我们都不允许 GC 收集具有此标志的对象,但在固定模式下,我们还可以在垃圾回收期间防止移动此类对象。据我所知,在普通模式下,并不直接将引用(内存地址)传递给非托管代码,而只是将 GC 描述符表中的索引传递给它。当非托管代码回调托管代码时,此索引将转换为当前/实际地址。我的头脑混乱,在 Google 和 Microsoft 中几乎没有详细信息,只有复制粘贴。

我的问题:

  1. 某些应用程序根(而不是弱)引用托管堆中的对象,没有更多的根。这意味着 GC 描述符表中相应的条目将具有 GCHandleType.Normal 标志吗?看起来并不是,因为 Jeffrey 说,“GC 不能删除对象,即使应用程序代码中可能没有对对象的引用。”但如果不是,那么这个表项会有哪个标志?同样,MyClass mc = new MyClass(),mc 在 GC 描述符表中对应的条目是否有 Normal 标志?如果不是,又是什么?
  2. 开发人员真正需要使用 GCHandleType.Normal 标志的时候是什么时候(以及如何,请给出简短的代码示例)?对于固定标志,我更清楚。
3个回答

6
GCHandle 文档中了解到:

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

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

如果您仅需要传递一个不透明句柄以便非托管代码可以将其传回而不访问它,则不需要固定对象,但仍需确保垃圾回收器不会将其删除。

考虑以下类:

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

如果您尝试像这样获取它的固定句柄:
MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Pinned);

当你看到这个异常信息时:

对象包含非原始或非可置入数据。

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

以下代码不会抛出异常:

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

由于您不能使用返回的句柄来获取地址。
因此,回答您的问题:
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); 

从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;
}

这个 ToInt32() 会破坏与 x64 的兼容性。 - SupinePandora43
@SupinePandora43:没错。但是当构建这样的代码时,您可能受到本机代码的位限制。当您将本机代码移植到x64时,您将不得不更改声明,从而使您最终能够将其向后传播为类型IntPtr。 - Joshua
然而,由于类型是正常的,这些都是不必要的。你可以使用字典。 - Joshua

4
作为经验法则,当您期望从本机代码(未访问)返回指针时,使用 Normal。当您期望从本机代码访问指针时,请使用 Pinned
这是因为当您将 Normal 对象传递到本机代码时,只能传递 IntPtrGCHandle(通过 GCHandle.ToIntPt 检索)。它只能通过 GCHandle.FromIntPt 在托管代码中复活回来。
关于其工作原理的详细解释可以在此处找到:https://blogs.msdn.microsoft.com/jmstall/2006/10/09/gchandle-tointptr-vs-gchandle-addrofpinnedobject/

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

GCHandleType.Normal由托管代码用于创建不会消失的对象。例如,一些Timer类会保持自己的实例处于活动状态,以便在放弃对其的最后一个引用时计时器不会停止。

据我所知,在正常模式下,并不传递直接引用(内存地址)给非托管代码,只传递了来自GC描述符表的索引。

这是不可能的,因为在进行PInvoke的时候并没有足够的信息来告诉你是否与要传递的对象相关联。即使它想这样做,也无法执行此操作。而且,非托管代码如何处理句柄表项呢?它不理解它。句柄表是CLR内部的。

一些应用程序根(而不是弱引用)引用托管堆中的对象,没有更多根引用。这是否意味着在GC描述符表中相应的条目将带有GCHandleType.Normal标志?看起来不是,因为Jeffrey说“即使从应用程序代码中没有引用,GC也不能删除对象”。但如果没有,则此表项将具有哪个标志?

它具有创建该GCHandle时传递的标志。只有存在GCHandle的条目才会在该表中进行跟踪。正常对象未被跟踪。


2
此外,非托管代码会如何处理句柄表项?我来解释一下 Jeffrey Richter 在他的例子中的意思:假设你有一个 delegate void(object state) 并且你想要原生代码通过这个委托回调给你,并使用你传递给原生代码的某个状态对象。在这种情况下,你可以使用 Normal 选项创建 GCHandle 对象,并将其(转换为 IntPtr)传递给原生代码。 原生代码不对句柄进行任何操作,它只是将句柄作为回调参数传递。 这使你能够从原生代码中获取相同的状态对象,而不限制 GC 固定它。 - vers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接