最近在研究VC多线程编程的问题,碰到了在子线程中调用主线程的窗口指针,使用了HWND* afxGetMainWnd(),如下面的线程函数:
程序编译能够通过,当你运行到::SetDlgItemText()函数时,你会遇到访问地址冲突的问题。这是因为,在子线程里获取的主窗口指针,并非真正的窗口指针,所以不能对其操作。
解决这个问题,你需要添加一个CWnd* m_pCWnd的全局变量,然后在主对话框的OnInitDialog()里面添加m_pCWnd = AfxGetMainWnd();最后修改上面出错的部分:
::SetDlgItemText(m_pCWnd->m_hWnd, IDC_TIME, strTime);
运行程序,OK了。
/*********************************************************
MFC工作线程的创建与控件控制
MFC的主界面是在界面线程运行的,如果在Button的响应函数中写了一个用时很长的函数,那么就要考虑运用工作线程,否则MFC程序主界面就会卡住不动,直到函数运行完才会响应消息。
开启工作线程的正确姿势
那就是用MFC封装好的线程函数AfxBeginThread。
CString strFolderPath = L"C:\\";
CWinThread* pThread = NULL;
pThread = AfxBeginThread((AFX_THREADPROC)BeginScan, szFolderPath, THREAD_PRIORITY_IDLE);//szFolderPath是参数,路径
DoEvent();
if(pThread)
{
do
{
dwRet = ::MsgWaitForMultipleObjects(1, &pThread->m_hThread, FALSE, INFINITE, QS_ALLINPUT);
if (dwRet != WAIT_OBJECT_0)
{
DoEvent();
}
} while ((dwRet != WAIT_OBJECT_0) && (dwRet != WAIT_FAILED));
}
if (pThread)
{
CloseHandle(pThread);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
其中DoEvent
void CMyDlg::DoEvent()
{
MSG msg;
if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) //取消息,检索应用程序的消息队列,PM_REMOVE取过之后从消息队列中移除
{
//发消息
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
1
2
3
4
5
6
7
8
9
10
如此方式开启工作线程,不会影响界面的操作响应。而且工作线程的权限为THREAD_PRIORITY_IDLE,不容易消耗过高CPU造成卡死。
参考链接:
https://blog.csdn.net/xl19900502/article/details/50342021
工作线程中控制控件
由于我们写的函数是运行在工作线程之下的,所以不能直接调用界面线程的主对话框类对象中的方法,否则会引起崩溃。
这时解决方法有以下几种:
一、把需要更改的控件变量或控件句柄,当作参数传到AfxBeginThread创建的函数中。
这样的缺点是如果需要更改的控件比较多时,需要传很多参数。要更改的控件比较少时可以这样搞。
二、向界面线程发送消息更改控件状态。
在工作线程中不能使用UpdateData来更新主线程中的数据,只能在主线程中使用。界面的东西,最好就是用主线程(界面线程)去执行相应的操作。要想在子线程(工作线程)里执行界面线程的操作,最好是向主线程发送消息。当然创建消息需要遵循固定的四步骤。
参考链接:https://blog.csdn.net/u011499425/article/details/52515216
三、在工作线程中重新获取一遍控件的句柄。
CWnd* edit = AfxGetApp()->GetMainWnd()->GetDlgItem(IDC_EDIT1);//获取主窗口编辑框控件的句柄
/*****************************************************
最近在使用 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() 来处理自定义的窗口消息。
以上的所有代码都只是作为测试用的,还不能真正体现出他们的作用,这里只做个方法的演示,读者只有在真的碰到类似的问题是,再使用此方法才能真正的体现出它的作用之处!
/************************************************************************
在一个窗口的某条信息处理函数中new了一个进度条控件对象。然后创建一个子线程,然后在子线程中delete这个控件对象。没想到每到这个delete的地方,程序就会崩溃。
提示:Debug Assertion Failed! xxx.exe
FIle:xxx.cpp
LINE:1019
For information on how your program can cause an assertion failure,see the Visual ...
我开始以为是内存越界或多次释放问题。断点跟踪的情况却说明指针和指针指向的值都是对的。为什么就不能delete呢?
现在才知道是不能在子线程中操控控件。正确的做法应该是在子线程中发送消息给窗口。让界面线程自己管理界面。
粘贴一下在网上搜的的解释:(还是不懂额,谁能解释一下)
不要在线程函数体内操作MFC控件,因为每个线程都有自己的线程模块状态映射表,在一个线程中操作另一个线程中创建的MFC对象,会带来意想不到的问题。更不要在线程函数里,直接调用UpdataData()函数更新用户界面,这会导致程序直接crash。而应该通过发送消息给主线程的方式,在主线程的消息响应函数里操作控件。
/*******************************************************************
MFC本身不是线程安全的,最好不要在线程中操作UI。如果不得不这么做,首先确保自己能做好同步。然后方法还是很多的。
发送消息。虽然操作UI可能会导致问题,但是发送消息却不会,利用消息启动实例的响应函数,然后从共享数据里取出数据进行操作。
把实例的句柄传给线程。在保证同步的情况下操作实例的方法。
/***********************************************************************************
首先让我说,我已经对此进行了大致的搜索,但是找不到具体的答案。
我的问题是理论上的,任何代码的运行都没有问题。 请考虑一个带有计时器事件和一个按钮(附加到OnClick事件)的简单MFC应用程序。
1 |
void SampleDlg::OnTimer(UINT_PTR nIDEvent) |
我的直觉表明,如果我在OnClick事件中睡觉,则主UI线程应该挂起,并且计时器事件不应启动。
1 |
void SampleDlg::OnClick() |
很好,但是如果我在OnClick内显示一个新的模式对话框,则计时器事件仍会发生。 这里有什么不同?
1 |
void SampleDlg::OnClick() |
编辑:我已包括GetCurrentThreadId()电话,使我想问的更清楚。
当我运行上面的代码时,msgbox和窗口标题都给我相同的线程ID:22012(例如)。 我的问题是,显示msgbox时,线程22012的PC / IP(程序计数器或指令指针)的值是多少?
MessageBox具有其自己的消息循环,就像所有模式对话框一样。
因此,它使用PeekMessage / GetMessage。 WM_TIMER和WM_PAINT消息是在消息循环执行时生成的伪消息。
这意味着MessageBox在内部调用GetMessage或PeekMessage,这导致线程仍然执行MessageBox,但是新消息将传递给您的窗口。