模板应用--UI线程与worker线程同步 模仿c# invoke

时间:2023-03-09 08:50:12
模板应用--UI线程与worker线程同步 模仿c# invoke

由之前的一篇博文 《UI线程与worker线程》《UI线程与worker线程》引出,UI线程与worker线程“串行化”在win32上实现是多么没有节操的事情,代码编写麻烦不说,编写过程中容易打断思路,还不易于维护,遇到这种重复性高的代码(即使操作步骤一样),就像眼里的沙,一刻都容忍不得,必须想出一种办法解决这种现状。

由于之前项目中需要临时用C#写个小工具,用来调试和测试,当中也遇到了类似的问题,其中发现,.net本身就提供了解决这类问题的方法,而且很简单,在worker线程执行处先判断当前Form对象的InvokeRequired属性,如果需要等待UI线程执行完,则创建一个delegate给invoke,妈蛋,MS那帮懒人,MFC就是一直不更新,只能自己动手了。

既然.net提供了这么一个方法可以解决这个问题,那win32程序自然也可以有这么一种方法(当然不是使用.net那种),实在没有的话模仿它,照猫话虎也行。好,动手干吧,实验一下又不会怀孕。在整理出需要关注哪些细节问题之前,先明确一下目的,换句话说,先明确一下到时这个实验结果怎么应用到工程当中。

既然有.net这样样板可供参考,那就按照它那样来,到时client代码应该写起来像这样:

void window_obj::create_and_run_thread(){
boost::thread t1(boost::bind(&window_obj::thread_func1, this));
boost::thread t2(boost::bind(&window_obj::thread_func2, this));
}
void window_obj::thread_func1(){
for (;;){
if (need_exit)
break;
// do something to ask for result
received_result1(result);
}
} void window_obj::thread_func2(){
for (;;){
if (need_exit)
break;
// do something to ask for result
received_result2(result1, result2);
}
} void window_obj::received_result1(result_type result1){
// 最好能用把这个条件分支用宏定义代替,要简单直观
if (required){
invoke(window_obj::received_result1, this, result1);
return;
} // do something with result 1 in UI thread
} void window_obj::received_result2(result_type result1, other_type result2){
// 然后在把这个条件分支用宏定义代替,要简单直观
if (required){
invoke(window_obj::received_result2, this, result1, result2);
return;
} // do something with result1 and result2 in UI thread
}

嗯,看起来像回事,接下来看怎么实现了。

1.需要有个可调用的东西用来判断当前是否需要invoke,即当前线程是否与UI线程相同,这最好办了;

    2.既然只能通过sendmessage实现,那invoke就是对sendmessage的封装,并且把window_obj::received_result这个member function用boost::function打包一下,通过指针传给sendmessage作为参数叫给UI线程调用;既然是sendmessage,就必须得自定义一个消息,然后在window_obj的消息响应里执行这个worker传过来的functor,好吧,就用RegisterWindowMessage这个API生成Message ID吧;

    3.既然要省去添加消息映射宏,那就得消息处理函数认识这个消息id,有两个方案可选消息钩子、修改这个窗口的窗口过程函数入口SetWindowLong,后一种简单一些;把原先的窗口过程函数用变量old_proc_保存起来,以便这个消息id之外的其他消息能正常响应,窗口过程函数又是一个全局函数或静态函数,那old_proc_变量也必须是全局或静态的,且这是在多线程环境,那必须对old_proc_变量值的修改进行加锁;

    4.考虑到对窗口调用SetWindowLong时要对old_proc_值的更改进行安全保护而加锁,为了把这种加锁解锁的开销降到最低,又要满足任何窗口类型都能使用这个类,把这个类设计成模板类是个不错的选择。

根据以上4点,基本满足了使用需求,先写出代码:

#include <map>
#include <set>
#include <boost/function.hpp>
#include <boost/thread.hpp> namespace invoke
{
/*
** to check if the calling thread is different from UI thread
*/
static bool required(){
return ::GetCurrentThreadId() != AfxGetApp()->m_nThreadID;
} ////////////////////////////////////////////////////////////////////////// typedef boost::function<void (void)> operate_func; ////////////////////////////////////////////////////////////////////////// template<class T>
class dispatch
{
static boost::mutex mtx_;
static WNDPROC old_proc_;
public:
static UINT msg_id_; protected: dispatch(){} static UINT wnd_proc(HWND hWnd, UINT msg, WPARAM w, LPARAM l)
{
if (msg == msg_id_){
// call the message function
(*(invoke::operate_func*)l)();
return 1;
} return ::CallWindowProc(old_proc_, hWnd, msg, w, l);
} public: static void request_invoke(HWND hWnd)
{
if (!old_proc_){
// maybe other thread is update the old_proc_ value
boost::unique_lock<boost::mutex> guard(mtx_);
if (!old_proc_)
// i am sure that the old_proc_ update operation won't conflict
old_proc_ = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)&wnd_proc);
}
}
}; template<class T>
boost::mutex dispatch<T>::mtx_; template<class T>
WNDPROC dispatch<T>::old_proc_ = NULL; template<class T>
UINT dispatch<T>::msg_id_ = ::RegisterWindowMessage(_T("INVOKE_MSG_DEFINE"));
} 然后使用的时候像这样:
void window_obj::received_result1(result_type result1){
// 最好能用把这个条件分支用宏定义代替,要简单直观
if (this == NULL || !::IsWindow(GetSafeHwnd()))
return;
if (invoke::required()){
invoke::dispatch<cls>::request_invoke(GetSafeHwnd());
UINT msg = invoke::dispatch<cls>::msg_id_;
invoke::operate_func f = boost::bind(&cls::func, this, result1);
::SendMessage(GetSafeHwnd(), msg, (WPARAM)0, (LPARAM)&f);
return;
} // do something with result 1 in UI thread
} 如果函数中有多个参数需要传递给UI线程,那也只需把要传递的参数在boost::bind(&cls::func, this, result1);末尾添加进去打包成boost::function就行了,那剩下的就是把它写成宏定义,使用的时候方便。
#define NEED_INVOKE_BEGIN(cls, func) \
if (this == NULL || !::IsWindow(GetSafeHwnd())) \
return; \
if (invoke::required()){ \
invoke::dispatch<cls>::request_invoke(GetSafeHwnd()); \
UINT msg = invoke::dispatch<cls>::msg_id_; \
invoke::operate_func f = boost::bind(&cls::func, this #define NEED_INVOKE_END \
); \
::SendMessage(GetSafeHwnd(), msg, (WPARAM)0, (LPARAM)&f); \
return; \
} #define NEED_INVOKE0(cls, func) \
NEED_INVOKE_BEGIN(cls, func) \
NEED_INVOKE_END #define NEED_INVOKE1(cls, func, a1) \
NEED_INVOKE_BEGIN(cls, func) \
, boost::ref(a1) \
NEED_INVOKE_END #define NEED_INVOKE2(cls, func, a1, a2) \
NEED_INVOKE_BEGIN(cls, func) \
, boost::ref(a1) \
, boost::ref(a2) \
NEED_INVOKE_END #define NEED_INVOKE3(cls, func, a1, a2, a3) \
NEED_INVOKE_BEGIN(cls, func) \
, boost::ref(a1) \
, boost::ref(a2) \
, boost::ref(a3) \
NEED_INVOKE_END

然后像这样使用

void window_obj::received_result1(result_type result1){
NEED_INVOKE2(window_obj, received_result1, result1); // do something with result 1 in UI thread
}

若要支持更多的参数个数,照猫画虎写多几个宏定义即可,不过通常一个函数要是函数参数过多,我也会抓狂。。。