4

在工作中,我们有一个本机 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. 数据、详细信息和状态是否会自动固定/保持活动状态?
  2. 我是否错过了正常运行所需的其他任何内容?

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

4

4 回答 4

2

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

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

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

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

此外,您可能想要验证调用 GCHandle.Alloc() 不应该在您的锁内。通过将它放在锁之外,您将有多个线程来分配内存。

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

于 2009-02-09T15:37:57.993 回答
1
  1. 我不确定您的 KeepAlive 的意义是什么,因为您已经释放了 GCHandle - 此时似乎不再需要数据?
  2. 与#1 类似,您为什么觉得需要调用 KeepAlive?您发布的代码之外的东西是我们没有看到的吗?
  3. 可能不是。如果这是一个同步的 P/Invoke,那么封送拆收器实际上将固定传入的变量,直到它返回。事实上,您可能也不需要固定数据(除非这是异步的,但您的构造表明它不是)。
  4. 不,没有遗漏任何东西。我认为您实际上添加的内容超出了您的需要。

编辑以回应原始问题编辑和评论:

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

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

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

于 2009-02-09T15:40:09.503 回答
0

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

于 2009-02-09T15:19:51.157 回答
0

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

于 2010-02-26T17:00:06.990 回答