安全句柄和句柄引用

7

在阅读了这个网站上的高票答案之后,我仍然觉得有些不清楚。

由于我对这个问题的理解可能是错误的,我首先会发布一份我所知道的内容摘要,以便在我错误时可以进行更正,然后发布我的具体问题:

有时候在编写托管代码时,我们必须将地址传递给非托管代码。这就是IntPtr的作用。然而,我们尝试确保两件相反的事情:a)保持指针(到地址)在GC中保持活动状态。b)在不需要它时释放它(即使我们忘记明确释放它)。

HandleRef实现了第一个功能,而SafeHandle实现了第二个功能。(我实际上是指列在此处的SafeHandle的派生类)。

我的问题:

  1. 显然,我想确认两者。那么我该如何获得它们的功能?(这是主要问题。)
  2. 这里MSDN(“调用托管对象”)看来,只有someObject.Handle可能会被GC回收,而独立的IntPtr则不会。但是,IntPtr本身是托管的!
  3. 如何在它超出范围之前对IntPtr进行GC(如此处所述)?

1
IntPtr 在超出作用域之前无法进行垃圾回收,但拥有实际资源的对象可以进行垃圾回收,从而使指针失效。 - Anton Savin
@AntonSavin 写一个方法创建一百万个IntPtr,然后调用它一千次 - 你不会遇到内存溢出异常。为什么?因为它们是受垃圾回收器管理的。不信吗? - ispiro
1
我在谈论不同的事情 - IntPtr指向的对象可能不再存在,因为它的所有者已经被GC回收了,但是在那个时候IntPtr可能仍然存在。 - Anton Savin
句柄不是指针,所以这个问题很混乱。迄今为止,传递指针最常见的方法是使用refout或通过传递对象。IntPtr无法进行垃圾回收。PInvoke marshaller非常擅长自动固定。 - Hans Passant
1个回答

10
我认为你把指针(IntPtrvoid*)和句柄(对Windows对象的引用)混淆了。不幸的是,句柄可以用IntPtr类型表示,这可能会令人困惑。 SafeHandle用于处理句柄。句柄不是指针,而是系统提供的表中的索引(有点像——它应该是不透明的)。例如,CreateFile函数返回一个HANDLE,适合与SafeFileHandle一起使用。SafeHandle类本身是一个包装器,用于包装Windows句柄,当SafeHandle被完成时,它将释放Windows句柄。因此,您必须确保保持对SafeHandle对象的引用,只要您想使用该句柄。
指针只是一个值。它是内存中对象的地址。IntPtr是一个struct,并且struct语义将使其按值传递(也就是说,每次将IntPtr传递给函数时,实际上都会复制IntPtr)。除非装箱,否则GC甚至不知道你的IntPtrHandleRef文档的重要部分是这样的:

HandleRef构造函数接受两个参数:表示包装器的Object和表示未托管句柄的IntPtr。Interop marshaler仅传递句柄到未托管代码,并保证在调用期间包装器(作为HandleRef构造函数的第一个参数传递)保持活动状态。

让我们看看MSDN示例:
FileStream fs = new FileStream("HandleRef.txt", FileMode.Open);
HandleRef hr = new HandleRef(fs, fs.SafeFileHandle.DangerousGetHandle());
StringBuilder buffer = new StringBuilder(5);
int read = 0;

// platform invoke will hold reference to HandleRef until call ends

LibWrap.ReadFile(hr, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(hr, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);

这相当于:
FileStream fs = new FileStream("HandleRef.txt", FileMode.Open);
var hf = fs.SafeFileHandle.DangerousGetHandle();
StringBuilder buffer = new StringBuilder(5);
int read = 0;

LibWrap.ReadFile(hf, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(hf, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);

// Since we no more have a HandleRef, the following line is needed:
GC.KeepAlive(fs);

但在这种情况下更好的解决方案是:

using(FileStream fs = new FileStream("HandleRef.txt", FileMode.Open))
{
    StringBuilder buffer = new StringBuilder(5);
    int read = 0;

    LibWrap.ReadFile(fs.SafeFileHandle, buffer, 5, out read, 0);
    Console.WriteLine("Read with struct parameter: {0}", buffer);
    LibWrap.ReadFile2(fs.SafeFileHandle, buffer, 5, out read, null);
    Console.WriteLine("Read with class parameter: {0}", buffer);
}

总之:
  1. For handles, use SafeHandle and make sure it's reachable until you don't need it anymore, at which point you either let the GC collect it or you dispose it explicitly (by calling the Dispose() method).

    For pointers, you make sure the pointed-to memory is pinned the whole time the native code can access it. You can use the fixed keyword or a pinned GCHandle to achieve this.

  2. IntPtr is a struct, as stated above, so it's not collected by the GC.

  3. It's not the IntPtr that's collected, it's the HWnd object that's exposing it that's no longer reachable at this point and is collectable by the GC. When finalized, it disposes the handle.

    The code from the referenced answer is:

    HWnd a = new HWnd();
    IntPtr h = a.Handle;
    
    // The GC can kick in at this point and collect HWnd,
    // because it's not referenced after this line.
    // If it does, HWnd's finalizer could run.
    // If it runs, HWnd will dispose the handle.
    // If the handle is disposed, h will hold a freed handle value,
    // which is invalid. It still has the same numerical value, but
    // Windows will already have freed the underlying object.
    // Being a value type, h itself has nothing to do with the GC.
    // It's not collectable. Think of it like it were an int.
    
    B.SendMessage(h, ...);
    
    // Adding GC.KeepAlive(a) here solves this issue.
    

    As for the object reachability rules, an object is considered as no longer used as soon as there's no more reachable references to the object. In the previous example, just after the IntPtr h = a.Handle; line, there is no other later usage of the a variable, therefore it is assumed this object is no longer used and can be freed anytime. GC.KeepAlive(a) creates such an usage, so the object remains alive (the real thing is a bit more involved since usage tracking is done by the JIT but this is good enough for this explanation).


SafeHandle没有像HandleRef那样的安全措施,对吗?

好问题。我想P/Invoke marshaler将在调用期间保持句柄的存活状态,但其拥有对象(如HWnd)仍然可能在调用期间显式地处理它。这是HandleRef提供的安全措施,你不能只使用SafeHandle来获得它。你需要确保句柄所有者(前面示例中的HWnd)自己保持存活。

但是HandleRef的主要目标是包装IntPtr,这是存储句柄值的旧方法。现在,SafeHandle已经成为句柄存储的首选方法。你只需要确保句柄所有者不会在P/Invoke调用期间显式处理句柄。


谢谢。我还在努力理解这个答案。但是,你为什么写道 “h 将会持有一个无效的值” - 你不是说它是按值传递的吗?现在 a 已经被收集起来了,没有什么能改变这一点。它应该仍然有效。 - ispiro
我理解的是否正确,对于数字3的答案是,即使未“正式”超出范围,当垃圾回收器发现它们不会再被访问时,它们仍然可以被垃圾回收。 - ispiro
所以你必须确保在你想要使用句柄的时候,SafeHandle对象的引用被保留下来。- 所以你是说我理解得正确,SafeHandle没有像HandleRef一样的安全措施,对吗? - ispiro
最近有点慢。一切都正确,但在某些情况下有许多可接受的方法。HandleRef 适用于不要忘记保留引用。但是可以使用 GC.KeepAlive 或任何其他方式完全实现相同的效果。SafeHandle 是确保即使在 pinvoke 深处中止线程(即在托管世界中从方法获得结果之前),也会释放句柄的唯一方法,并且同时 release-handle-api 应该是无线程的。但是 SafeHandle 对您没有提供任何有用的东西,例如如果您尝试包装 HWND(因为 HWND 无法从随机线程中销毁)。 - Dmitry Azaraev
现在我对这个问题的理解更深了,所以我重新回顾一下。总结你的答案 - 应该使用SafeHandle并保留对引用对象的引用,这样就可以了。我理解得对吗?(根据这个答案 - 也许甚至不需要保留引用 - 在引入SafeHandle之前,...防止被GC清除是程序员的责任。) - ispiro
@ispiro SafeHandle 确保在另一个线程的 P/Invoke 调用中使用时,如果在使用过程中被处理,则句柄将在调用返回之后释放(文档),因此您可以放心使用它。至少我从“调用 Close 或 Dispose 方法允许释放资源。如果其他线程正在使用相同的安全句柄对象,则可能不会立即发生,但一旦不再是这种情况,就会发生。”这句话中理解到了这一点。 - Lucas Trzesniewski

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