本文仅是实验和实证的结果.(深层原因需要有机会了解ProcessMessages的实现)
1.缘起
Hotfox在移植到BCB环境下,以支持客户端开发后,在应用开发测试时发现:移植的本地管理模块在增加角色时会阻塞在一个简单的ShowMessage调用上.程序执行流程大致如下:
- 生成发送570-Request请求
- 执行同步调用,消息处理者为TFrmAddRole::Do572函数(CFormMsgHandler类型)
- 在TFrmAddRole::Do572函数中,处理成功提示时阻塞.(测试时在函数入口增加一行ShowMessage语句即导致阻塞)
2.解决思路
阻塞的原因是消息的处理是由后台线程调用的(bbox_c模块的HandleInput),且消息处理函数包含了VCL操作。目标是满足客户端开发要求,并且不想影响现有的应用代码。需要把后台线程的函数调用在框架层次转移到主线程上执行。
以下是对插件和TForm消息处理函数的转移执行的说明:.
对于CFormMsgHandler,
用
SendMessage(CBasePlugInModule::async_fc_wnd_,WM_ASYNC_FUNC_CALL,2,(LPARAM)new CFormFuncCallPara(form_,func_,msg->Duplicate()));
取代
(form_->*func_)(msg);
对于插件,用:
SendMessage(async_fc_wnd_,WM_ASYNC_FUNC_CALL,1,(LPARAM)new CPluginFuncCallPara(fp,p->mod_,in,&out,&or));
取代:
code = (p->mod_->*fp)(in, out,or);
(异步化一词来自最初的准备采用PostMessage的想法,已不合适,)
异步化函数调用消息由内部的TfrmAsyncFuncCall窗体处理.处理代码如下:
void __fastcall TfrmAsyncFuncCall::OnReceive(TMessage &Message)
{
switch(Message.WParam) {
case 1: {
CPluginFuncCallPara *fcp = (CPluginFuncCallPara*)Message.LParam;
fcp->Call();
delete fcp;
}
break;
case 2: {
CFormFuncCallPara *fcp = (CFormFuncCallPara*)Message.LParam;
fcp->Call();
delete fcp;
}
break;
case 3: {
CShowMessagePara *fcp = (CShowMessagePara*)Message.LParam;
///< @todo 外部指定如何显示信息,以适应不同的应用
MessageShow(0,fcp->text_.c_str(),fcp->caption_.c_str(),MSGERROR);
delete fcp;
}
break;
}
}
迁移处理的相关定义如下:
#define WM_ASYNC_FUNC_CALL WM_USER+2
///< AsyncFuncCall: 异步化函数调用
< WM_FUNC_CALL用于在多线程环境下把可能需要访问VCL对象的函数进行异步化处理的自定义窗口消息
///< WPARAM : 1-插件函数 2-Form函数 3-信息提示
///< LPARAM :根据WPARAM对应不同的结构体对象
//---------------------------------------------------------------------------
///< 插件函数异步调用消息参数
struct CPluginFuncCallPara {
MSGFUNC fp_;
CPlugInModule *mod_;
CWrappedMsg<> *in_;
vector<CWrappedMsg<> *> *out_;
DISPATCH_RESULT *or_;
CPluginFuncCallPara(MSGFUNC fp,CPlugInModule *mod,CWrappedMsg<> *in,vector<CWrappedMsg<> *> *out,DISPATCH_RESULT *or):
fp_(fp),mod_(mod),in_(in),out_(out),or_(or) {
}
int Call() {
return (mod_->*fp_)(in_, *out_,*or_);
};
};
//---------------------------------------------------------------------------
///< Form函数异步调用消息参数
struct CFormFuncCallPara {
TForm *form_;
FormMsgHandleFunc func_; ///< 消息处理函数
CMsg *msg_;
CFormFuncCallPara(TForm *form,FormMsgHandleFunc func,CMsg *msg):form_(form),func_(func),msg_(msg) {
}
int Call() {
return (form_->*func_)(msg_);
}
};
3.验证
测试发现修改后仍然阻塞,发生在以下位置(对于插件处理函数)SendMessage(async_fc_wnd_,WM_ASYNC_FUNC_CALL,1,(LPARAM)new CPluginFuncCallPara(fp,p->mod_,in,&out,&or));
没有进入到TfrmAsyncFuncCall::OnReceive中.
4.再究
测试发现由于同步调用导致:int ret = CBasePluginModule::sc_->call(msg,result,0,0,timeout);
同步调用发生在窗体上,主线程等待服务器的返回并处理后唤醒。
在处理返回的消息时SendMessage的消息没有机会被处理。
需要使用TApplication的ProcessMessage来中断当前操作,处理消息队列中的消息。
由于同步调用是另起一个线程(do_syncall_func)通过条件变量等待返回的消息被处理后被唤醒或超时或取消的。代码如下:
/// 执行同步调用
int HTX_Syncall::call(CMsg *msg,int &result,CMsg **ppmsg,SC_CALLBACK fp,int tv) {
HANDLER_THREAD_INFO *ti = prepare(msg);
SC_THREAD_PARA tp;
tp.sc = this;
tp.tv = tv;
tp.result = 0;
tp.thr_ = ACE_Thread::self();
ACE_hthread_t hthread;
ACE_Thread::spawn(do_syncall_func,&tp,THR_NEW_LWP|THR_JOINABLE,0,&hthread);
{
ACE_GUARD_RETURN(ACE_Thread_Mutex,guard,ti->lock_ready_cond_var_,-1);
ti->ready_cond_var_->wait();///< 等待do_syncall_func进入wait
}
if (fp)
(*fp)(msg);
else {
HTX_NETWORK::instance()->SendMsg(msg);
}
ACE_Thread::join(hthread);
threads_.unbind(ti->cmd_id_); ///< 从等待队列中清除
delete ti;
result = tp.result;
if (ppmsg) {
*ppmsg = tp.ret_msg_;
}
else {
if (tp.ret_msg_) tp.ret_msg_->Release();
}
return tp.wait_result;
}
在do_syncall_func函数中加入Application->ProcessMessage(通过回调cb_func_设置)后,仍然阻塞.
是否意味着,Application->ProcessMessage不能在非主线程中使用?
5.试验ProcessMessages
目的是确认在非主线程中执行ProcessMessage没有预期效果.试验方案:
主线程中创建2个线程,主线程组塞。
创建的2个线程,一个线程PostMessage或SendMessage,一个线程执行Applicaiton->ProcessMessage.
情形1:
DWORD __stdcall TestThreadProc(void *arg) {
while(1) {
Application->ProcessMessages();
Sleep(1000);
};
return 0;
}
DWORD __stdcall TestThreadProc2(void *arg) {
while(1) {
SendMessage(form2->Handle,WM_ASYNC_FUNC_CALL,0,0); ///< 阻塞在此
Sleep(30);
}
return 0;
}
void __fastcall TForm1::Button3Click(TObject *Sender)
{
CreateThread(0,0,TestThreadProc,0,0,0);
CreateThread(0,0,TestThreadProc2,0,0,0);
while(1) {
Sleep(1000);
};
}
情形2:正常
DWORD __stdcall TestThreadProc(void *arg) {
while(1) {
/// Application->ProcessMessages(); ///< 不论是否有此行都正常
Sleep(1000);
};
return 0;
}
DWORD __stdcall TestThreadProc2(void *arg) {
while(1) {
SendMessage(form2->Handle,WM_ASYNC_FUNC_CALL,0,0);
Sleep(30);
}
return 0;
}
void __fastcall TForm1::Button3Click(TObject *Sender)
{
CreateThread(0,0,TestThreadProc,0,0,0);
CreateThread(0,0,TestThreadProc2,0,0,0);
while(1) {
Application->ProcessMessages();
Sleep(1000);
};
}
结论:TApplication的ProcessMessages在非主线程中执行没有作用.
6.再试
修改同步调用函数,使ProcessMessages在主线程上执行。
代码修改成如下内容时,问题得以解决.
/// 执行同步调用
int HTX_Syncall::call(CMsg *msg,int &result,CMsg **ppmsg,SC_CALLBACK fp,int tv) {
HANDLER_THREAD_INFO *ti = prepare(msg);
SC_THREAD_PARA tp;
tp.sc = this;
tp.tv = tv;
tp.result = 0;
tp.thr_ = ACE_Thread::self();
ACE_hthread_t hthread;
ACE_Thread::spawn(do_syncall_func,&tp,THR_NEW_LWP|THR_JOINABLE,0,&hthread);
{
ACE_GUARD_RETURN(ACE_Thread_Mutex,guard,ti->lock_ready_cond_var_,-1);
ti->ready_cond_var_->wait();///< 等待do_syncall_func进入wait
}
if (fp)
(*fp)(msg);
else {
HTX_NETWORK::instance()->SendMsg(msg);
}
#ifdef HTX_WINDOWS
///< 对于客户端程序,在主线程上的同步调用会导致窗口消息的阻塞.如果返回的消息在其它线程(如HTX_Scheduler任务线程)处理过程中,使用SeneMessage导致死锁
///< 在此情况下,cb_func_的作用是处理消息队列中的消息(BORLANDC中执行Application->ProcessMessage)
///< @note 在do_syncall_func函数中执行cb_func_没有效果,因为****Application->ProcessMessage不能在非主线程中执行****
do {
DWORD status = 0;
if (!GetExitCodeThread((void*)hthread,&status))
break;
(*cb_func_)(0);
if (status!=STILL_ACTIVE) {
break;
}
Sleep(1000);
} while(1);
#else
ACE_Thread::join(hthread);
#endif
threads_.unbind(ti->cmd_id_); ///< 从等待队列中清除
delete ti;
result = tp.result;
if (ppmsg) {
*ppmsg = tp.ret_msg_;
}
else {
if (tp.ret_msg_) tp.ret_msg_->Release();
}
return tp.wait_result;
}