1.线程之间的通信简介
一般而言,在一个应用程序中(即进程),一个线程往往不是孤立存在的,常常需要和其它线程通信,以执行特定的任务。如主线程和次线程,次线程与次线程,工作线程和用户界面线程等。这样,线程与线程间必定有一个信息传递的渠道。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的。线程间的通信涉及到4个问题:
(1) 线程间如何传递信息
(2) 线程之间如何同步,以使一个线程的活动不会破坏另一个线程的活动,以保证计算结果的正确合理
(3) 当线程间具有依赖关系时,如何调度多个线程的处理顺序
(4) 如何避免死锁问题
在windows系统中线程间的通信一般采用四种方式:全局变量方式、消息传递方式、参数传递方式和线程同步法。下面分别作介绍。
2.全局变量方式
由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,我们建议使用volatile 修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。
实例演示
该实例采用全局变量来控制时间显示线程的显示格式,比较简单。
主要的代码如下:
.h头文件
//线程函数声明
DWORD WINAPIThreadProc(LPVOIDlpParam);
protected:
HANDLE m_hThread;//线程句柄
DWORD m_nThread;//线程ID
.cpp实现文件
volatileBYTE m_nShowFlag = 31;//定义全局变量,用于控制显示时间的格式。volatile 修饰符的作用是告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中。
//创建显示时间的线程,参数无
m_hThread =CreateThread(NULL,0,ThreadProc,NULL,0,&m_nThread);
//线程执行函数,用于实时显示时间,并按规定格式显示
DWORD WINAPIThreadProc(LPVOID lpParam)
{
while(m_nShowFlag)
{
CTime time;
CString strTime,strFormat;
time=CTime::GetCurrentTime();
strFormat = "%H:%M";
if (m_nShowFlag&2)
{//日期
strFormat = "%Y-%m-%d" + strFormat;
}
if (m_nShowFlag&4)
{//秒钟
strFormat += ":%S";
}
if (m_nShowFlag&8)
{//周数
strFormat += "%W";
}
if (m_nShowFlag&16)
{//星期
strFormat += "%a";
}
strTime=time.Format(strFormat);
::SetDlgItemText(AfxGetApp()->m_pMainWnd->m_hWnd,IDC_STATIC_TIME,strTime);
Sleep(100);
}
return 0;
}
运行效果:
工程源码下载地址:
http://download.csdn.net/detail/cbnotes/4962315
欢迎大家修改和指正。
注意事项:
(1) 全局变量最好放在.CPP文件的起始处,而不要放在.h头文件中,否则将出现重复链接的编译错误。定义全局变量时最好显式初始化,默认初始值为零。
(2) 注意语句
::SetDlgItemText(AfxGetMainWnd()->m_hWndIDC_STATIC_TIME,strTime);在VC6.0中可以通过,但在VC2008却出错,这是因为在VC2008中不支持AfxGetMainWnd()->m_hWnd来获取HWND,但可以采AfxGetApp()->m_pMainWnd->m_hWnd来获取。所以上面的语句更改为:
::SetDlgItemText(AfxGetApp()->m_pMainWnd->m_hWnd,IDC_STATIC_TIME,strTime);
(3) 用全局变量方式来实现多线程的通信比较简单实用,单注意最好不要多个线程对它进行修改,否则将可能出错,这将在后面会具体讲解。
3.参数传递方式
该方式是线程通信的官方标准方法,多数情况下,主线程创建子线程并让其子线程为其完成特定的任务,主线程在创建子线程时,可以通过传给线程函数的参数和其通信,三类创建线程的函数都支持参数的传递(哪三类?看前面的介绍吧!)。所传递的参数是一个32位的指针,该指针不但可以指向简单的数据,而且可以指向结构体或类等复杂的抽象数据类型。
实例演示
下面将分别简单演示三类创建线程时提供参数传递的方法。
主要的代码如下:
.h头文件
//线程函数声明
DWORD WINAPI ThreadFunc1(LPVOID lpParam);//线程函数
void ThreadFunc2(void *pArg); //线程函数
UINT ThreadFunc3(LPVOID lpParam);//线程函数
//全局函数
POINT GetRandPoint();//得到随机点的坐标
//结构体定义,用于向线程传递多个参数
struct threadInfo
{
HWND hWnd;//主窗口句柄
COLORREF clrPen;//画笔颜色
};
.cpp实现文件
//开始创建线程:创建个线程
void CMultThreadComm2Dlg::OnStart(void)
{
//线程:使用win32 API 创建:实时显示时间
m_hThread1 = CreateThread(NULL,0,ThreadFunc1,&m_stTime.m_hWnd,0,NULL);//
//线程:使用CRT 创建:随机画线
m_info.hWnd = m_hWnd;
m_info.clrPen =RGB(255,0,0);
_beginthread(ThreadFunc2,0,&m_info);
//线程:使用MFC线程函数创建:显示进度
m_pThread = AfxBeginThread(ThreadFunc3,&m_ctrlProgress);
}
//停止/开始
void CMultThreadComm2Dlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
CString szTitle;
GetDlgItemText(IDC_BUTTON1,szTitle);
if (szTitle == "停止")
{//停止
g_bRun = false;
SetDlgItemText(IDC_BUTTON1,"开始");
}
else
{//开始
g_bRun = true;
OnStart();
SetDlgItemText(IDC_BUTTON1,"停止");
}
}
//线程执行函数:实时显示当前的时间
DWORD WINAPI ThreadFunc1(LPVOID lpParam)
{
HWND *hWnd = (HWND*)lpParam;
//CWnd *pWnd = AfxGetApp()->m_pMainWnd;//当没有传递参数时,可以用该api实现
CWnd *pWnd = CWnd::FromHandle(*hWnd);
char tmpbuf[128] ={'0'};
time_t t;
while(g_bRun)
{
t = time(NULL);
strftime(tmpbuf,128,"%Y-%m-%d%a %I:%M:%S %p",localtime(&t));
pWnd->SetWindowText(tmpbuf);
Sleep(500);
}
return 0;
}
//线程执行函数:随机画线
void ThreadFunc2(void *pArg)
{
threadInfo *threadinfo= (threadInfo*)pArg;
CWnd *pWnd = CWnd::FromHandle(threadinfo->hWnd);
CDC *pDC = pWnd->GetDC();
CPen pen(PS_SOLID,2,threadinfo->clrPen);
pDC->SelectObject(&pen);
pDC->SetROP2(R2_NOTXORPEN);
while(g_bRun)
{
POINT StartPos= GetRandPoint();
POINT EndPos = GetRandPoint();
//
CString str;
str.Format("%d,%d : %d,%d\n",StartPos.x,StartPos.y,EndPos.x,EndPos.y);
TRACE(str);
pDC->MoveTo(StartPos);
pDC->LineTo(EndPos);
Sleep(100);
pDC->MoveTo(StartPos);
pDC->LineTo(EndPos);
Sleep(100);
}
DeleteObject(pDC);
}
//线程执行函数:显示进度
UINT ThreadFunc3(LPVOIDlpParam)
{
CProgressCtrl *pProgress= (CProgressCtrl*)lpParam;
while(g_bRun)
{
pProgress->StepIt();
Sleep(500);
}
return 0;
}
//得到随机点
POINT GetRandPoint()
{
POINT Point;
Point.x = rand()%439;
Point.y = rand()%208;
return Point;
}
运行效果:
工程源码下载地址:
http://download.csdn.net/detail/cbnotes/4984274
欢迎大家修改和指正。
注意事项:
(1) 注意三类线程的创建方法和各自的线程函数的格式(返回类型各不一样)。
(2) 注意三类参数的传递:单个参数、多参数(结构体),复杂参数(类)。
(3) 采用参数传递方式进行线程间的通信只适用于主线程向从线程的通信。
(4) 不知道大家看出该程序的一个BUG没有(大家可以下载工程源码,编译并运行,可以很明显的发现。),就是线程二的随机画线线程,原意是随机画线并清除,但运行发现第一次画线时总是没有被清除掉,为什么?望大家动脑,知道的可以留言,大家一起学习和讨论!
4.消息传递方式
在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的非常简单。我们可以在一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。利用Windows操作系统的消息驱动机制,当一个线程发出一条消息时,操作系统首先接收到该消息,然后把该消息转发给目标线程,接收消息的线程必须已经建立了消息循环。该方式可以实现任意线程间的通信,所以是比较常见和通用的方式。
系统也提供了线程间发送消息的专用函数:PostThreadMessage()。用户先定义一个用户消息,在一个线程调用PostThreadMessage()函数,在消息接收线程响应该消息,大体和常用的小子响应处理差不多。只是消息映射为ON_THREAD_MESSAGE而不是ON_MESSAGE
如果线程有窗体,还可以使用通用的消息发送函数PostMessage()和SendMessage()这两个函数。注意这两则的区别,PostMessage()是异步函数,调用后函数立即返回,而SendMessage()是同步函数,要等相应的消息响应完成后才返回。
关于PostThreadMessage()、PostMessage()和SendMessage()函数介绍请参考MSDN,在此就不多吃一举了。
对于PostThreadMessage()的用法:
先定义一个用户消息,如:
#define WM_THREADMSG WMUSER+100
在需要发送消息的线程调用PostThreadMessage()函数,如:
::PostThreadMessage(idThread,WM_THREADMSG,parm1, parm2);
或者
m_pThread->PostThreadMessage(WM_THREADMSG,parm1, parm2);
其中: idThread为消息接受线程的ID,parm1, parm2为要传递的参数,m_pThread消息接受线程指针。
在接受消息线程中(或消息处理线程中),先定义消息响应函数,如:
afx_msg void OnThreadMessage(WPARAM wParam,LPARAM lParam);
然后在消息映射表中添加该消息的映射,如:
ON_THREAD_MESSAGE(WM_THREADMSG,OnThreadMessage)
最后,实现该消息函数,如:
//显示消息处理函数
void XXXXThread::OnThreadMessage(WPARAM wParam,LPARAM lParam)
{
;//消息处理
}
/
对于PostMessage()/SendMessage()的用法:和上面的差不多,略有不同。
首先也是先定义一个用户消息,如:
#define WM_THREADMSG WMUSER+100
在需要发送消息的线程调用PostMessage()/SendMessage()函数,如:
::PostMessage(hWnd, WM_THREADMSG, parm1, parm2);//发送消息
或者
PWnd->PostMessage(WM_THREADMSG, parm1, parm2);
或者
::SendMessage(hWnd, WM_THREADMSG, parm1, parm2);//发送消息
或者
PWnd->SendMessage(WM_THREADMSG, parm1, parm2);
其中: hWnd为接受消息的窗口句柄,parm1, parm2为要传递的参数,pWnd消息接受窗口指针。
在接受消息线程中(或消息处理线程中),先定义消息响应函数,如:
afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam);
然后在消息映射表中添加该消息的映射,如:
ON_MESSAGE(WM_THREADMSG, OnMyMessage)
最后,实现该消息函数,如:
//消息处理函数
LRESULT XXXXWnd::OnMyMessage(WPARAM wParam,LPARAM lParam)
{
;//消息处理
return 0;
}
实例演示
该实例主要时计算正整数1-N的累加,并采用单独的线程来计算累积,并创建另一个用户界面线程来实时显示累加的进度,其中涉及到一些线程间的通信,该实例主要采用了消息的方式,并结合前面已经介绍的两种通信方式,比较具有演示和学习的价值。
主要源码:
主线程头文件:
DWORD WINAPI ThreadFunc(LPVOIDlpParam);//线程函数
protected:
HANDLE m_hThread;//线程句柄
CProgressThread *m_pThread;//用户界面线程句柄
DWORD m_nThreadID;//线程ID号
主线程实现文件:
//开始计算
void CMultThreadComm3Dlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
UpdateData(TRUE);//更新数据
g_bStop = false; //重置
m_hThread = CreateThread(NULL,0,ThreadFunc,&m_nRange,0,NULL);//创建计算线程
GetDlgItem(IDC_BUTTON1)->EnableWindow(FALSE);//防止重复计算
SetDlgItemText(IDC_STATIC_RESULT,"计算中...");
}
//结果显示:两种结果:,正常,,被中途中止
LRESULT CMultThreadComm3Dlg::OnResult(WPARAMwParam,LPARAMlParam)
{
if (wParam == 1)
{//被中途中止
SetDlgItemText(IDC_STATIC_RESULT,"被中止啦!");
}
else
{//正常结束
SetDlgItemInt(IDC_STATIC_RESULT,lParam);
}
GetDlgItem(IDC_BUTTON1)->EnableWindow(TRUE);//使能下次计算按钮
CloseHandle(m_hThread);//关闭线程句柄,注意一定要关闭它
return 0;
}
//线程执行函数:计算
DWORD WINAPI ThreadFunc(LPVOIDlpParam)
{
UINT *pRange =(UINT*)lpParam;//得到参数值
long nResult= 0L;//计算结果值
bool bStop = false;//标识是否中途被中止
CProgressThread *m_pThread= (CProgressThread*)AfxBeginThread(RUNTIME_CLASS(CProgressThread));//创建显示进程
m_pThread->PostThreadMessage(WM_PROGRESS,0,*pRange);//传递参数,进度条的范围
for(int i=0;i<=*pRange;i++)//开始计算
{
if (g_bStop)
{//中途取消了
bStop = true;
break;//退出循环
}
nResult += i;
m_pThread->PostThreadMessage(WM_PROGRESS,1,i);//进度
Sleep(10);//为了演示效果,特意延时
}
//完成
::PostMessage(AfxGetApp()->m_pMainWnd->m_hWnd,WM_RESULT,bStop,nResult);//显示结果
m_pThread->PostThreadMessage(WM_PROGRESS,2,0);//结束进度
return 0;
}
累加线程头文件:
CProgressDlg *m_pProgressDlg;//进度条对话框
unsigned int m_nRange;//进度条的范围
累加线程实现文件:
BOOL CProgressThread::InitInstance()
{
// TODO: 在此执行任意逐线程初始化
//创建非模式进度显示对话框
m_pProgressDlg = newCProgressDlg();
m_pProgressDlg->Create(IDD_DIALOG1);
m_pProgressDlg->ShowWindow(SW_SHOW);
return TRUE;
}
BEGIN_MESSAGE_MAP(CProgressThread, CWinThread)
ON_THREAD_MESSAGE(WM_PROGRESS, &CProgressThread::OnThreadMsg)
END_MESSAGE_MAP()
// CProgressThread 消息处理程序
//线程消息处理函数
void CProgressThread::OnThreadMsg(WPARAM wParam,LPARAM lParam)
{
if (wParam == 0)
{//初始化进度条
m_nRange = lParam;
m_pProgressDlg->m_ProgressCtrl.SetRange(0,lParam);
}
else if (wParam == 1)
{//显示进度
m_pProgressDlg->m_ProgressCtrl.SetPos(lParam);//显示进度
CString str;
str.Format("%d%%",int((float)lParam/m_nRange*100));
m_pProgressDlg->m_stValue.SetWindowText(str);//显示百分数
str.Format("计算处理中,请稍等... %d/%d cbNotes",lParam,m_nRange);//显示实时的当前操作
m_pProgressDlg->SetWindowText(str);
}
else
{//完成,退出进度条
m_pProgressDlg->CloseDlg();//结束进程对话框
AfxEndThread(0);//终止本线程,也可以使用PostQuitMessage(0);
}
}
进度条对话框类
//系统消息处理
void CProgressDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (nID == SC_CLOSE)//关闭
{//拦截关闭按钮消息
if (AfxMessageBox("确定要【终止】本次计算吗?",MB_YESNO|MB_APPLMODAL|MB_ICONQUESTION|MB_DEFBUTTON2)==IDYES)
{
g_bStop = true;//中途取消操作,结束计算线程。
}
return;
}
CDialog::OnSysCommand(nID, lParam);
}
//关闭窗口
void CProgressDlg::CloseDlg(void)
{
OnCancel();
}
//重载OnCancel():注意非模式对话框的退出
void CProgressDlg::OnCancel()
{
// TODO: 在此添加专用代码和/或调用基类
DestroyWindow();//
//CDialog::OnCancel();
}
void CProgressDlg::PostNcDestroy()
{
// TODO: 在此添加专用代码和/或调用基类
CDialog::PostNcDestroy();
delete this;
}
运行结果:
工程源码下载地址:
http://download.csdn.net/detail/cbnotes/5007011
欢迎大家修改和指正。
注意事项:
(1) 注意线程消息的映射方式:
ON_THREAD_MESSAGE(WM_PROGRESS, &CProgressThread::OnThreadMsg)
和一般的窗口消息映射不同,不要搞错了。
(2) 注意用户界面线程的退出,计算完后要记得结束该线程,有专门的函数:
AfxEndThread(0),也可以使用PostQuitMessage(0)。这两种方式都是比较安全的。不推荐强制中止线程,否则会出现意想不到的问题。
(3) 该实例还实现了执行过程按退出按钮时的处理方法,采用了全局变量的
方式来控制计算线程的中途退出,我自认为是一种比较好的控制方式。我开始采用强制中止的方式会出现消息延后的现象(就是在退出按钮的处理函数里向主线程发送消息postmessage),一直没有找到原因,有兴趣的朋友可以试试其它的退出方式供大家讨论学习。
(4) 注意非模式对话框退出的处理方法,和模式对话框的退出不一样的。
(5)用CreateThread()函数创建线程将返回一个线程句柄,通过该句柄你可以控制和操作该线程,当你不用时可以一创建该线程后就关闭该句柄,有专门的函数CloseHandle()。关闭句柄并代表关闭线程,只是你不能在外部控制该线程(比如,提前结束,更改优先级等)。在线程结束后,系统将自动清理线程资源,但并不自动关闭该句柄,所以线程结束后要记得关闭该句柄,你可能要问为什么你这么强调要关闭该句柄,是因为该句柄(HANDLE)是内核对象,内核对象的管理是操作系统管理的,具体你可以查查相关资料,我在此就不啰嗦了。
5.线程同步法
还可以通过线程同步来实现线程间通信。例如有两个线程,线程A写入数据,线程B读出线程A准备好的数据并进行一些操作。这种情况下,只有当线程A写好数据后线程B才能读出,只有线程B读出数据后线程A才能继续写入数据,这两个线程之间需要同步进行通信。关于线程同步的方法和控制是编写多线程的核心和难点,方法也比较多,在此就不具体讲解和实例演示,具体内容我将在接下来的文章中一一道来,敬请继续关注我的更新,谢谢。