您的位置:首页 > 产品设计 > UI/UE

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

2013-08-27 18:20 429 查看
由之前的一篇博文 《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
}


若要支持更多的参数个数,照猫画虎写多几个宏定义即可,不过通常一个函数要是函数参数过多,我也会抓狂。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: