P / Invoke,Pinning和KeepAlive最佳实践

在工作中,我们有一个本机C代码,负责读取和写入专有的平面文件数据库。 我有一个用C#编写的包装器,它将P / Invoke调用封装到OO模型中。 自项目启动以来,P / Invoke调用的托管包装器的复杂性大大增加。 有趣的是,当前的包装器做得很好,但是,我认为我实际上需要做更多的事情以确保正确的操作。

答案带来了几个笔记:

  1. 可能不需要KeepAlive
  2. 可能不需要GCHandle固定
  3. 如果您确实使用GCHandle,请尝试…最终该业务(但CER问题未解决)

以下是修订代码的示例:

[DllImport(@"somedll", EntryPoint="ADD", CharSet=CharSet.Ansi, ThrowOnUnmappableChar=true, BestFitMapping=false, SetLastError=false)] [ReliabilityContract(Consistency.MayCorruptProcess, Cer.None)] internal static extern void ADD( [In] ref Int32 id, [In] [MarshalAs(UnmanagedType.LPStr)] string key, [In] byte[] data, // formerly IntPtr [In] [MarshalAs(UnmanagedType.LPArray, SizeConst=10)] Int32[] details, [In] [MarshalAs(UnmanagedType.LPArray, SizeConst=2)] Int32[] status); public void Add(FileId file, string key, TypedBuffer buffer) { // ...Arguments get checked int[] status = new int[2] { 0, 0 }; int[] details = new int[10]; // ...Make the details array lock (OPERATION_LOCK) { ADD(file.Id, key, buffer.GetBytes(), details, status); // the byte[], details, and status should be auto // pinned/keepalive'd if ((status[0] != 0) || (status[1] != 0)) throw new OurDatabaseException(file, key, status); // we no longer KeepAlive the data because it should be auto // pinned we DO however KeepAlive our 'file' object since // we're passing it the Id property which will not preserve // a reference to 'file' the exception getting thrown // kinda preserves it, but being explicit won't hurt us GC.KeepAlive(file); } } 

我(修订)的问题是:

  1. 数据,详细信息和状态是否会自动固定/ KeepAlive’d?
  2. 我是否错过了正确操作所需的任何其他内容?

编辑:我最近发现了一个图表,这引起了我的好奇心。 它基本上表明,一旦你调用P / Invoke方法, GC就可以抢占你的本机代码 。 因此,虽然可以同步进行本机调用,但GC 可以选择运行并移动/移除我的内存。 我想现在我想知道自动锁定是否足够(或者甚至是否运行)。

  1. 我不确定你的KeepAlive是什么意思,因为你已经释放了GCHandle – 似乎那时候不再需要这些数据了?
  2. 与#1类似,为什么你觉得你需要打电话给KeepAlive? 除了您发布的代码之外我们还没有看到的东西吗?
  3. 可能不是。 如果这是一个同步P / Invoke,则编组程序将实际固定传入的变量,直到它返回。 事实上,您可能不需要固定数据(除非这是异步,但您的构造表明它不是)。
  4. 不,没有错过。 我认为你实际上增加了超过你需要的东西。

编辑回复原始问题编辑和评论:

该图表仅显示GC 模式更改,该模式对固定对象没有影响。 在编组期间 ,类型可以固定或复制 ,具体取决于类型。 在这种情况下,您使用的是一个字节数组, 文档说这是一个blittable类型 。 您将看到它还明确指出“作为优化,仅包含blittable成员的blittable类型和类的数组被固定而不是在编组期间被复制。” 这意味着数据在调用期间被固定,如果GC运行,则无法移动或释放数组。 状态也是如此。

传递的字符串略有不同,字符串数据被复制,指针在堆栈上传递。 这种行为也使它不受收集和压缩的影响。 GC无法触摸副本(它对它一无所知)并且指针位于堆栈上,GC不会影响。

我仍然没有看到调用KeepAlive的重点。 据推测,该文件不可用于收集,因为它已传入方法并且有一些其他根(声明它的地方)可以使它保持活动状态。

除非您的非托管代码直接操作内存,否则我认为您不需要固定该对象。 固定基本上告知GC它在收集周期的紧凑阶段期间不应该在存储器中移动该对象。 这对于非托管内存访问非常重要,其中非托管代码期望数据始终位于传入时的相同位置.GC操作的“模式”(并发或抢占)应该对固定没有影响作为固定行为规则的对象适用于任一模式。 .NET中的编组基础结构试图了解它如何在托管/非托管代码之间整理数据。 在这种特定情况下,您正在创建的两个arrays将在编组过程中自动固定。

除非您的非托管ADD方法是异步的,否则可能不需要调用GC.KeepAlive。 GC.KeepAlive仅用于防止GC在长时间运行期间回收它认为已死的对象。 由于文件作为参数传入,因此可能在调用托管Add函数后在代码中的其他位置使用,因此不需要GC.KeepAlive调用。

您编辑了代码示例并删除了对GCHandle.Alloc()和Free()的调用,这是否意味着代码不再使用它们? 如果您仍在使用它,则锁(OPERATION_LOCK)块中的代码也应该包含在try / finally块中。 在你的finally块中,你可能想要做这样的事情:

 if (dataHandle.IsAllocated) { dataHandle.Free(); } 

此外,您可能想要validation调用GCHandle.Alloc()不应该在您的锁内。 通过让它锁定锁定,你将有多个线程在分配内存。

就自动固定而言,如果数据在编组过程中自动固定,则它会被固定,如果在非托管代码运行时发生一个GC收集周期,它将不会被移动。 我不确定我是否完全理解您关于继续调用GC.KeepAlive的原因的代码注释。 unamanged代码是否实际为file.Id字段设置了值?

一个直接的问题似乎是,如果抛出exception,你将永远不会调用dataHandle.Free(),从而导致泄漏。

阅读托管和本机代码互操作性的最佳实践,并使用PInvoke Interop Assistant