2017-08-24 114 views
0

我们正在为我们的硬件传感器套件实施许多SDK。C++ DLL - 跨线程回调

成功为我们的传感器获得了一个可用的C API后,我们现在开始测试SDK的艰巨任务,以确保我们没有引入任何致命错误,内存泄漏或竞争条件。

我们的一位工程师报告说,在开发测试应用程序(Qt Widgets应用程序)时,挂钩到从DLL内的单独线程执行的回调时发生了问题。

这里是回调原型:

#define API_CALL __cdecl 

typedef struct { 
    // msg fields... 
    DWORD dwSize; 
    // etc... 
} MSG_CONTEXT, *PMSG_CONTEXT; 

typedef void (API_CALL *SYS_MSG_CALLBACK)(const PMSG_CONTEXT, LPVOID); 

#define API_FUNC __declspec(dllexport) 
API_FUNC void SYS_RegisterCallback(SYS_MSG_CALLBACK pHandler, LPVOID pContext); 

而且它附着在Qt的如下:

static void callbackHandler(const PMSG_CONTEXT msg, LPVOID context) { 
    MainWindow *wnd = (MainWindow *)context; 

    // *wnd is valid 
    // Call a function here 
} 

MainWindow::MainWindow(QWidget *parent) { 
    SYS_RegisterCallback(callbackHandler, this); 
} 

我的问题是:是回调对创造或线程上执行执行它的线程?无论哪种情况,我都需要某种同步方法。谷歌搜索导致了大量的C#示例,这并不是真正需要的。

正在考虑的一件事是使用SendMessagePostMessage函数,而不是沿着回调路线。

任何人都可以提供任何建议,请问如何使用回调来实现跨线程安全?或者,消息泵路由是否适用于基于Windows的SDK?

+0

“我的问题是:是在创建它的线程还是在执行它的线程上执行的回调?”这对你自己来说是微不足道的 - 在回调函数内部保留一个断点,并在调试器击中它时查看你所在的线程。 – MrEricSir

+0

@MrEricSir,道歉 - 我当时没有在调试代码,而是另一位工程师。但是,是的,这可能是微不足道的发现。 – weblar83

回答

1

任何人都可以提供任何建议,请问如何使用回调来实现跨线程安全性?

是 - 它在Qt中完成的标准方式:从任何线程发出信号。 connect一个插槽/仿函数,它在生活在你想要的目标线程中的对象的上下文中执行。我们也可以做一些相当的事情,不明确地使用信号/时隙,但功能相同 - 毕竟,slot/functor调用是通过QMetaCallEvent中的线程进行的。

您显示的代码通常会导致未定义的行为,因为您尝试使用除主线程之外的任何线程使用GUI对象(MainWindow),并且您调用的方法不可能是线程安全的。

正确的做法是使MainWindow的方法是线程安全的,或者以线程安全的方式调用它。

下面显示了一种可能的方法;有关isSafepostCall的实现请参见this answer

static void callbackHandler(const PMSG_CONTEXT msg, LPVOID context) { 
    auto wnd = reinterpret_cast<MainWindow*>(context); 
    if (!isSafe(wnd)) 
     return postCall(wnd, [=]{ callbackHandler(msg, context); }); 

    // we're executing in wnd's thread context, any calls on wnd 
    // will be thread-safe 
    wnd->foo(); 
} 

MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { 
    SYS_RegisterCallback(callbackHandler, this); 
} 

又见this question about invoking functors across threadsthis question about invoking methods thread-safely

另一种解决方案是直接提供此功能,并有一个SYS_RegisterThreadPumpCallback会注册一个回调,就像SYS_RegisterCallback做,但会假定接收线程运行的消息泵(如Qt的主线程执行,任何QEventLoop -spinning线程也是)。该回调将作为消息传递给隐藏窗口,然后通过函数指针执行实际调用。

还有另一种解决方案是SYS_RegisterThreadAPCCallback,当接收线程可提醒时,将使用QueueUserAPC来调用回调。这比向消息泵发送消息要好一些,但是如果用户代码在意外的地方可以提醒并且意外地重新输入了不可重入的代码(注意:重入和线程安全是正交的概念),可能会导致麻烦。

我个人会欢迎在Windows上提供所有三种回调的API。

+0

感谢您的评论。该DLL不是一个Qt库,只是一个使用Windows/Linux API和常用导出函数的标准DLL。我们的SDK是平台不可知的,因此不想在实现应用程序之外的任何其他应用程序中使用特定于Qt的构造。 – weblar83

+0

@ weblar83我演示了如何实现应用程序以处理跨线程回调。这基本上是你的Qt用户应该做的。您可能还会提供将在给定线程中执行回调的回调注册。这只适用于Windows。 –

+0

@ weblar83在Linux上,可警示原语是posix sync stuff或文件描述符。在那里它可能有助于为用户提供他们可以等待的文件描述符,因为它与Qt('QSocketNotifier')以及所有其他事件循环(wxWindows,gtk,xlib)很好地接口。 Posix的东西没有任何接口,是毫无价值的恕我直言(IIRC没有应用程序开发工具包“混合”posix同步原语与他们的事件循环)。 –

1

我的问题是:在创建它的线程或执行它的线程上执行的回调?

执行它的线程。

任何人都可以提供任何建议,请问如何使用回调来实现跨线程安全?

受互斥锁保护的简单std::queue<message>是最简单的解决方案。 Here就是一个很好的例子。

有回调没有做什么,但排队消息,并从您的主线程消耗它。

+0

感谢您的建议。根据您提供的解决方案,队列将如何处理?这是在一个计时器上完成,还是实施应用程序需要处理这个队列? – weblar83

+0

你大概有一个主循环的地方。只需在循环的每次迭代开始或结束时使用队列中的消息。 – Frank