代码之家  ›  专栏  ›  技术社区  ›  user7116

P/Invoke、Pinning和Keepalive最佳实践

  •  4
  • user7116  · 技术社区  · 15 年前

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

    答案带来的几个注意事项:

    1. 可能不需要保留字
    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 can preempt your native code . 因此,当本机调用可以同步进行时,GC 能够 选择运行并移动/删除我的内存。我想现在我想知道自动钉住是否足够(或者它是否运行)。

    4 回复  |  直到 14 年前
        1
  •  1
  •   ctacke    15 年前
    1. 我不确定你的keepalive的意义是什么,因为你已经释放了teh gcmhandle——在那一点上似乎不再需要数据了?
    2. 与1类似,为什么你觉得你需要称为Keepalive?Tehre是不是在你发布的代码之外,我们看不到?
    3. 可能不会。如果这是一个同步P/Invoke,那么封送拆收器将实际地固定传入变量,直到它返回。事实上,您可能也不需要固定数据(除非这是异步的,但您的构造建议不是这样)。
    4. 不,没有遗漏。我认为你实际添加的比你需要的更多。

    根据原始问题编辑和评论进行编辑:

    该图简单地显示了 模式 更改后,模式对固定对象无效。类型可以是 pinned or copied during marshaling ,取决于类型。在本例中,您使用的是字节数组,其中 docs say is a blittable type . 您将看到它还特别指出,“作为优化,在封送期间固定了只包含Blittable成员的Blittable类型和类的数组,而不是复制。”这意味着在调用期间固定了数据,如果GC运行,它将无法移动或释放数组。地位也是如此。

    传递的字符串稍有不同,将复制字符串数据,并在堆栈上传递指针。这种行为也使它不受收集和压缩的影响。GC不能接触副本(它不知道副本的内容),指针在堆栈上,GC不会影响。

    我还是不明白叫keepalive的意义。该文件可能不可用于收集,因为它已被传递到该方法中,并且有一些其他的根(在声明它的地方)可以使它保持活动。

        2
  •  2
  •   Scott Dorman    15 年前

    除非非托管代码直接操纵内存,否则我认为您不需要固定对象。pinning本质上通知GC,在收集周期的压缩阶段,它不应该在内存中移动该对象。这只对非托管内存访问很重要,因为非托管代码希望数据始终位于传入时的相同位置。GC操作的“模式”(并发或抢先)不应影响固定对象,因为固定的行为规则适用于这两种模式。.NET中的封送处理基础结构试图巧妙地处理托管/非托管代码之间的数据封送。在这种特定情况下,您正在创建的两个数组将在编组过程中自动固定。

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

    您编辑了代码示例并删除了对gcHandle.alloc()和free()的调用,这是否意味着代码不再使用这些调用?如果您仍在使用它,那么锁(operation_lock)块中的代码也应该包装在一个try/finally块中。在finally块中,您可能希望执行如下操作:

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

    另外,您可能想验证调用gcHandle.alloc()不应该在锁内。通过让它离开锁,您将有多个线程来分配内存。

    就自动固定而言,如果数据在封送处理过程中自动固定,那么它将被固定,并且如果在非托管代码运行期间发生了GC收集循环,则不会在该循环中移动它。我不确定是否完全理解您关于继续调用gc.keepalive的原因的代码注释。未管理的代码是否为file.id字段设置了值?

        3
  •  0
  •   Maurice Flanagan    15 年前

    一个直接的问题似乎是,如果抛出异常导致泄漏,则永远不会调用datahandle.free()。