当我们在开发需要实时采集数据和实时显示处理结果的程序时,通常会采用多线程。通过采用多线程,使采集数据的任务和显示处理结果的任务各自独立运行,例如通过从线程采集数据,通过主线程显示处理结果,可以达到程序的实时性要求,但是前提是必须实现主线程和从线程之间的实时通讯。最常用的办法是通过共享存储区,使主、从线程都能够实时地存取资源,但是这种方法不够灵活,并会出现资源共享冲突。本文要介绍的方法正是在其基础之上,通过自定义消息来实现主、从线程之间的灵活通讯,并采用临界区来解决共享存储区的存取冲突。下面将结合在Visual C++中的一个实例对该方法进行详细介绍。
一 、创建主线程界面
1.在Visual C++开发环境下新建一个基于对话框的MFC程序,取项目名ThreadSample,为对话框添加六个编辑框控件,且将IDOK的标题改为“开始”,将IDCANCEL的标题改为“停止”并使其失效。 如下图所示:
2.为每个编辑框控件添加保护型成员变量,如下表所示。
控件ID |
成员变量类型 |
成员变量名称 |
IDC_EDIT1 |
CString |
m_sData1 |
IDC_EDIT2 |
int |
m_iData2 |
IDC_EDIT3 |
double |
m_dData3 |
IDC_EDIT4 |
CString |
m_edit1 |
IDC_EDIT5 |
int |
m_edit2 |
IDC_EDIT6 |
double |
m_edit3 |
二、定义实现主、从线程通讯的消息和数据结构
1.定义从线程发给主线程的消息:
#define MESSAGEFROMTHREAD (WM_USER+1001)
2.定义主线程发给从线程的启动和停止消息:
#define STARTTHREAD (WM_USER+1002)// 启动
#define STOPTHREAD (WM_USER+1003)// 停止
3.定义主、从线程需要处理的数据结构,它是主、从线程共同需要的各种数据类型的集合,本例中定义如下:
struct THREADDATA
{
CString sData1;
int iData2;
double dData3;
};
三、创建从线程
1.在类CThreadSampleDlg的头文件中添加下面公有成员变量:
HANDLE m_hThread;
DWORD m_ThreadID;
2.在类CThreadSampleDlg的OnInitDialog()成员函数中创建从线程,代码如下:
m_hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread_Fun, (LPVOID)this->m_hWnd, NULL, &m_ThreadID);
if ((m_hThread == NULL))
{
MessageBox("从线程创建失败!", NULL, NULL);
exit(-1);
}
3.定义从线程函数,代码和注释如下:
UINT Thread_Fun(void* hWnd);// 声明从线程函数
UINT Thread_Fun(void* hWnd)
{
bool b_run_or_stop = false;//是否启动数据处理
THREADDATA thread_data;//线程要处理的数据
MSG msg;//线程的消息
while(1)
{
if(b_run_or_stop)//如果启动数据处理
{
EnterCriticalSection(&cs);//进入临界区
thread_data.sData1.MakeReverse();//处理数据
thread_data.iData2++; //本例只是对数据进行简单处理
thread_data.dData3+=0.2; //其原理同样适于复杂的数据处理过程
LeaveCriticalSection(&cs);//离开临界区
::PostMessage((HWND) hWnd, MESSAGEFROMTHREAD, (WPARAM) &thread_data, (LPARAM) 0 );//数据处理完毕,通知主线程
}
if(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{//检查线程的消息队列,是否有下面的自定义消息
switch(msg.message)
{
case WM_QUIT://退出从线程
ExitThread(10);
case STARTTHREAD:
//启动数据处理,从线程从消息中提取其将要处理的数据
thread_data.sData1 = ( (THREADDATA*) msg.wParam )->sData1;
thread_data.iData2 = ( (THREADDATA*) msg.wParam )->iData2;
thread_data.dData3 = ( (THREADDATA*) msg.wParam )->dData3;
b_run_or_stop = true;
break;
case STOPTHREAD://暂停数据处理
b_run_or_stop = false;
break;
default:
break;
}
}
}
return 0;
}
四、添加主线程的消息处理函数
1.为“开始”和“停止”分别添加处理函数,代码入下
void CThreadSampleDlg::OnStart()
{//给线程发启动消息,传送其需要的数据,并使其启动数据处理过程
UpdateData(true);
GetDlgItem(IDCANCEL)->EnableWindow(true);//激活停止按钮
GetDlgItem(IDOK)->EnableWindow(false); //使开始按钮失效
// m_Data为头文件中定义的THREADDATA类型成员变量
m_Data.sData1=m_sData1;
m_Data.iData2=m_iData2;
m_Data.dData3=m_dData3;
::PostThreadMessage(m_ThreadID, STARTTHREAD, (WPARAM) &m_Data, 0);// m_Data的地址作为参数发给从线程
}
void CThreadSampleDlg::OnStop()
{//给线程发暂停消息,使其暂停数据处理过程
GetDlgItem(IDCANCEL)->EnableWindow(false); //使停止按钮失效
GetDlgItem(IDOK)->EnableWindow(true); //激活开始按钮
::PostThreadMessage(m_ThreadID, STOPTHREAD, 0, 0);
}
2.为来自从线程的自定义消息添加消息处理函数:
首先在类CThreadSampleDlg的头文件的AFX_MSG宏中,添加函数声明:
//{{AFX_MSG(CThreadSampleDlg)
……
afx_msg void OnMessageFromThread(WPARAM wParam, LPARAM lParam);
……
//}}AFX_MSG
再在类CThreadSampleDlg的实现文件的BEGIN_MESSAGE_MAP宏中添加消息映射
//{{AFX_MSG_MAP(CThreadSampleDlg)
……
ON_MESSAGE(MESSAGEFROMTHREAD,OnMessageFromThread)
……
//}}AFX_MSG_MAP
最后添加消息处理函数的实现代码:
void CThreadSampleDlg::OnMessageFromThread(WPARAM wParam, LPARAM lParam)
{//主线程从MESSAGEFROMTHREAD消息的参数中提取经过处理的数据
m_edit1 = ( (THREADDATA*)wParam )->sData1;
m_edit2 = ( (THREADDATA*)wParam )->iData2;
m_edit3 = ( (THREADDATA*)wParam )->dData3;
UpdateData(false);//实时更新显示经过处理的数据
}
五、采用临界区解决资源的共享冲突
由于主线程在收到MESSAGEFROMTHREAD消息后要读取参数wParam(事实上是从线程的THREADDATA类型变量thread_data的地址),而此时从线程完全可能正在对该地址对应的数据进行处理,从而出现存取冲突。因此应该采取线程同步技术,通常用到的有临界区(Critical Section)、互斥对象(Mutex)和信号量(Semaphore)。本文将采用临界区控制资源的存取,解决共享冲突。临界区其实是轻量级的互斥变量,它对于同一进程内的多线程是更好的选择。
本例中临界区的具体实现过程如下:
首先定义一个全局的静态的临界区变量
static CRITICAL_SECTION cs;
然后在CThreadSampleDlg::OnInitDialog()函数中对该临界区初使化
::InitializeCriticalSection(&cs);
再将引起资源存取冲突的代码加入临界区,本例中即需将从线程函数进行数据处理的代码加入临界区(代码见上面列出的线程函数代码的下划线部分)。
最后在出现退出时销毁临界区,本例即在OnClose()函数添加代码:
::DeleteCriticalSection(&cs);
这样一来,当从线程进入临界区,对变量thread_data进行处理时,主线程就不会对其进行访问了,从而不再引起资源的存取冲突。