最近在使用 VC 开发软件时需要用到多线程同步来解决开发过程中遇到的问题。本来以为只要象控制台程序一样,在主线程创建子线程,并设置好相应的对象事件就能解决问题,但是等到真正做起来,才在实践中发现原来事情并没有我想象的那么简单。以下我介绍一下我在开发过程中遇到的问题。
我的 对话框程序是这样设计的,我把大部分需要用到的子控件都在主线程的对话框先设计好,然后,由于我需要实时监控软件的运行情况,并在对话框的一个静态控件实时显示软件的运行情况;也就是说,我需要通过在主线程创建的子线程来控制对话框程序的子控件。哈哈,问题来啦,就在这里。
起初,我的线程函数是这样设计的,我在主线程那里直接把子线程需要控制的子控件的变量直接通过参数传给子线程,然后直接在子线程那里控制子控件。本来,想想是觉得没问题的,但是不管我怎么测试,程序都不能正常执行,为了寻找原因,我直接把断点定位到子线程里,然后在子线程里面一步步的调试,一直调试到更改子控件的那语句之前都没问题,就唯独那句更改子控件的代码,每次到那里,整个程序就卡在那里,调试也调试不了,比如像下面这样的代码:
// 在主线程创建子线程
CWinThread* powerThread = AfxBeginThread(Thread_PowerOn, (LPVOID)this, THREAD_PRIORITY_NORMAL, 0, NULL, NULL);
// 子线程入口函数
UINT Thread_SetVoltageOfThePowerOn( LPVOID lpParameter )
{
。。。。。。
CSonyPeaceDlg* pDlg = (CSonyPeaceDlg*)lpParameter;
pDlg ->GetDlgItem(IDC_STATIC_COMMANDRESULT) ->SetWindowTextA(_T("打开电源。。。"));
。。。。。。
}
当程序执行到红色的那行代码时,程序就完全卡在那里不能往下执行了。
出现这个问题,一开始也很纳闷,后来一直在网上搜索问题的原因,才知道,原来,在 MFC 编程中子线程是不能直接访问主线程里的控件对象的,这样极容易造成访问异常,导致消息混乱程序卡死,MSDN 中也有说明,子线程直接访问主线程的控件对象是不安全的;为此,微软也提供了相应的解决方案:
1、通过传递控件句柄 HWND 给子线程,来对控件进行访问;
2、通过消息传送机制,也就是通过在子线程里给主线程传递访问控件对象的消息,然后在主线程的消息响应函数里面对控件对象进行操作。
以上这两种说法,可能是我一时半会还没理解过来,不知道它说的是两种解决方法,还是两种方法得结合起来用。最后我都试过,非得把它们都结合起来使用才能让程序达到我需要的效果。
在这里先插一下话题,一开始我是以为是两种都可行的解决方法,尤其是第一种方法最简单,所以我试了一下,只是在主线程那里把控件句柄传递给子线程,然后在子线程那里通过传递过来的控件句柄对控件对象进行访问,但还是不行,还是出现上面提到的现象,整个程序呈现卡死状态。如下面的代码:
// 这里我传递的是主窗口的句柄m_hWnd
powerThread = AfxBeginThread(Thread_PowerOn, (LPVOID)m_hWnd),THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL);
// 子线程入口函数
UINT Thread_SetVoltageOfThePowerOn( LPVOID lpParameter )
{
。。。。。。
HWND hwnd = (HWND)lpParameter;
::SetDlgItemTextA(hwnd, IDC_STATIC_COMMANDRESULT, _T("打开电源..."));
。。。。。。
}
同样,程序运行到红色那句代码是就又卡在那里了。
最后,我就把上面提到的两种说法结合起来用,在主线程里传递句柄给子线程,然后再子线程里通过给主线程发送消息给主线程来改变访问控件对象。如以下代码:
// 创建打开电源的线程
powerThread = AfxBeginThread(Thread_PowerOn, (LPVOID)this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL);
// 子线程入口函数
UINT Thread_SetVoltageOfThePowerOn( LPVOID lpParameter )
{
。。。。。。
CSonyPeaceDlg* pDlg = (CSonyPeaceDlg*)lpParameter;
//将要执行的命令发送出去
::PostMessageA( pDlg -> m_hWnd, UM_OPENPOWER, (WPARAM)(LPCSTR)_T("打开电源。。。"), (LPARAM)(LPCTSTR)_T("Put on power...") );
}
注意,为了方便,我直接把对话框程序的 指针this传递给子线程。
其中 UM_OPENPOWER是自定义的变量,需要自己定义。以下我顺便说下如何添加自定义消息的方法,以方便和我一样的小鸟级学者快速入门,呵呵!笑一个吧!
在源文件 ***Dlg.cpp 开头添加如下代码:
#define UM_OPENPOWER WM_USER+100 //自定义一个消息
在头文件 ***Dlg.h 添加消息映射函数声明:
public:
afx_msg LRESULT OnOpenPower(WPARAM WParam, LPARAM LParam);
在源文件 ***Dlg.cpp 开头添加消息映射,如下:
在源文件 ***Dlg.cpp 添加消息映射函数的代码:
这样就完成了自定义消息映射的添加工作。
接下来就是开始测试是否能够正常工作。读者可以自己再添加一个按钮控件,然后在按钮的消息映射里面把创建子线程的那行代码添加进去,以此来测试以上代码,会发现程序工作正常。这样就完成了通过子线程来访问主线程的控件对象的任务,只不过是间接访问而已。
道理说破了,其实也很简单对吧,呵呵!
这里我再说明一下,为了测试方便,我创建的子线程函数时 C***Dlg 类的友元函数,函数声明如下:
friend UINT Thread_PowerOn( LPVOID lpParameter );
需要将以上代码放到 C***Dlg 类的定义里面。
另外,我在子线程的入口函数里进行消息传送时,使用的是 PostMessageA()函数,而不是SendMessage() 函数,这里说明一下,这里需要用到的就是PostMessageA()函数,要是用SendMessage()函数的话,会发生阻塞的现象。这两个函数的主要区别就是,前者将消息发送出去后就会立即返回,后者需要等相应的消息映射函数处理完成后才能返回,所以会一直阻塞在那个地方。具体说明还请读者自己 百度 或 MSDN 一下!
附:以上只是将消息发送出去,然后等操作系统自己调用 WindowProc() 函数,再根据具体的消息而映射到相应的消息响应函数处对相应的控件进行处理,所以可能无法及时的处理,因为在整个程序的执行过程中会有相当多的 消息 等待着系统去处理。
针对以上问题,我们也可以根据需要重载WindowProc() 函数,然后根据需要在相应的地方调用它。比方说,我自己重载后的WindowProc() 函数的代码如下所示:
然后们可以在相应的地方根据需要调用此函数,就可以及时的对自定义的消息进行处理,如:我用一个按钮来对其进行测试:
这样就完成了自己根据需要在特定的时刻直接调用窗口处理函数 WindowProc() 来处理自定义的窗口消息。
以上的所有代码都只是作为测试用的,还不能真正体现出他们的作用,这里只做个方法的演示,读者只有在真的碰到类似的问题是,再使用此方法才能真正的体现出它的作用之处!
希望此篇文章对读者能有所收获,哈哈!