在C#中等待C++事件 - 作为任务或等待句柄。

4

我主要使用.NET进行编程,喜欢它的异步/并发原语,如Tasks、ResetEvents等。今天第一次修改C++程序,并理解了整个构建过程(我更新了LigthningDB.NET项目至0.9.14)。但我仍然缺乏C++知识。

我想为LMDB项目添加一个新功能(以满足自己的需求),即通知系统(类似于Redis):

  • 我希望有一个光标返回一个可等待对象,在其表中每次发生数据更改时发出信号。
  • 我想获取一些数据与信号一起(指向数据结构的指针),例如键或键+值。
  • 这个对象将与光标一起处理,是光标的一部分,但它会在光标引用的DB中发出更改信号。
  • 这必须在跨平台运行,并且可以使用任何脏的Windows特定方法来完成任务。
典型用例是一个作者和N个读者等待新消息。这将允许IPC和快速持久化2合1。LMDB支持来自不同进程的并发读取(而Esent和LevelDB则不支持)。
从C++中可以使用System.Threading原语,并且我知道如何使用WaitHandle进行阻塞调用。有没有一种方法使其异步?有一系列关于异步同步原语的精彩文章,但它们使用的是TaskCompletionSource,而这仅适用于.NET内部。是否可能为本机互操作创建类似的解决方案?
一种解决方案可能是命名管道或套接字,将更改传输到一个侦听器/调度程序(Redis或Rhino.Queues样式),但性能会受到影响:写操作将不得不分配、复制和推送数据,并且数据将不得不传输-比传递指向已经在内存中的数据结构的指针要糟糕得多。
另一个选择是将监听光标移动到键并发送信号。信号后,C#监听器将知道光标在更新的键处具有值。这种方式解决了数据传输部分的问题,但使用WaitHandles会阻塞——在我的用例中,阻塞比套接字[分配/复制/延迟]组合更糟糕。

还有更好的选择吗?

其他问题:

  1. 我在重新发明轮子吗?
  2. 是否可以指向.NET程序等待(非阻塞)C/C++信号的开源库(如果存在)?
  3. 我应该为此工作流使用LMDB吗? Windows是我的首选,我一直在尝试使LevelDB正常工作(停止尝试)。 LMDB的替代方案是否更好,包括信号传递?

更新

我在文档中找到了此方法

public static Task WaitOneAsync(this WaitHandle waitHandle)
{
    if (waitHandle == null) throw new ArgumentNullException("waitHandle");

    var tcs = new TaskCompletionSource<bool>();
    var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle, 
        delegate { tcs.TrySetResult(true); }, null, -1, true);
    var t = tcs.Task;
    t.ContinueWith(_ => rwh.Unregister(null));
    return t;
}

doc 中的 ThreadPool.RegisterWaitForSingleObject 说明:

等待操作由线程池中的线程执行。当对象的状态变为 signaled 或超时时间到达时,工作线程将执行委托。... 等待线程使用 Win32 的 WaitForMultipleObjects 函数来监视已注册的等待操作。

我的理解是这两个是不同的线程,如果没有信号,第一个等待线程是唯一会阻塞的吗?


我担心解决方案还需要再谷歌5分钟,而翻译这段话却花费了更多的时间。你仍然想知道这是否是“解决方案”吗?http://msdn.microsoft.com/en-us/library/hh873178(v=vs.110).aspx#WaitHandles - V.B.
我对于 RegisterWaitForSingleObject 的效率了解不多。我猜测它是基于IO完成端口的,因此应该是相当高效的。我认为没有专门等待该句柄的线程。可以考虑让其他进程发送一个1字节的命名管道消息(即虚拟消息)。然后,可以使用NamedPipeStream.ReadAsync来进行完全异步等待。 - usr
还要注意的是,异步等待通常会更加消耗CPU资源,并且具有更高的延迟。也许整个问题由于这个原因而无意义。 - usr
我有非常多的服务员,肯定有几百个甚至更多。我承受不起阻塞。但是 LMDB 的一个关键特性是它拥有数据,因此传递数据的指针并从中读取内存不会分配或复制数据。这是我试图达到的低 hanging fruit。 - V.B.
我能否通过管道/套接字将指针作为PtrInt传递?这样,我就可以使用从管道/套接字接收到的指针地址,并通过该地址访问内存。如何确保地址是固定的?据我所知,LMDB使用固定的内存映射 - 这足以保证指针有效吗? - V.B.
此外,向管道/套接字写入数据可能会减缓数据库写入速度。 - V.B.
2个回答

3
RWFSO使用特殊的线程池线程等待多个句柄。每个线程内置的句柄限制为63,因此效率不如IOCP。虽然MREs可以用来解决这个问题(实际上,MREs几乎可以解决所有问题...),但我不建议使用基于句柄的解决方案。
C++没有"事件"的概念。传统的方法是使用回调函数指针(现在结合Boost.Bind和Boost.Function使用起来不那么痛苦)。更现代的方法是使用Boost.Signals2。

1
回调函数可以通过调用“TaskCompletionSource”的“SetResult”方法与任务异步模式很好地配合使用。 - Ben Voigt
我相信他正在尝试跨进程引发事件。需要通知数据库进程和客户端进程。 - usr
用户:我同意。@V.B.:指针在进程间不起作用。由于地址空间虚拟化,相同的指针值对于不同的进程意味着不同的含义。 - Stephen Cleary
有一个选项可以使用固定的内存映射,或者我可以从零开始使用偏移量,但这样做不太好。我正在考虑在C端使用命名信号量和键/值指针队列。在仅追加模式下,甚至不需要队列,只需要新未见值的数量。此外,我可以制定一些调度方案,减少等待线程的数量。63是个好数字,感谢您提供的信息! - V.B.
@StephenCleary 顺便说一下,非常感谢您在博客和GitHub上提供的所有文章和其他信息。即使在这个问题之前,我也从您那里学到了很多 :) (最后一个问题:是否有类似的纯C解决方案示例,而不是C++/Boost?) - V.B.
显示剩余2条评论

0
更新:存在一个逻辑缺陷:要使用C回调,必须有一个单独的C进程和几个C#进程。但是使用MMF时,只有2个不同的C进程之间共享内存。因此,我需要像Redis一样的C服务器(但我们已经有了Redis!)或进程间信号传递。
我有可靠同步和Redis的经验,这是问题的根源-我想要一个类似的解决方案,但带有持久性的本地机器。
重新编写具有命名MREs/AREs的Redis逻辑可能是最简单的方法。正如Stephen所说,MRE可以做任何事情。

(任何投票都应该投给Stephen Cleary的答案,这里只是一个例子)

答案是使用TaskCompletionSource和回调来设置其结果。

回调的示例:

C#:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate int CompareCallback(int a, int b);

[SuppressUnmanagedCodeSecurity]
class NativeMethod
{

    [DllImport("CppDynamicLinkLibrary.dll", CharSet = CharSet.Auto)]
    public static extern int CompareInts(int a, int b, CompareCallback cmpFunc);

}

C:

// Type-definition: 'PFN_COMPARE' now can be used as type
typedef int (CALLBACK *PFN_COMPARE)(int, int);

// An exported/imported stdcall function using a DEF file
// It requires a callback function as one of the arguments
int __stdcall CompareInts(int a, int b, PFN_COMPARE cmpFunc)
{
    // Make the callback to the comparison function

    // If a is greater than b, return a; 
    // If b is greater than or equal to a, return b.
    return ((*cmpFunc)(a, b) > 0) ? a : b;
}

示例代码:https://code.msdn.microsoft.com/windowsdesktop/CSPInvokeDll-b05779d0


你应该总是typedef实际对象,而不仅仅是指向对象的指针。这样可以让你在实际对象声明中使用typedef。例如: `typedef int (CALLBACK PFN_COMPARE)(int, int);int __stdcall CompareInts(int a, int b, PFN_COMPARE *cmpFunc)` - hyc

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