线程间的通信
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;
}
执行效果:
project源代码下载地址:
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 ",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;
}
执行效果:
project源代码下载地址:
http://download.csdn.net/detail/cbnotes/4984274
欢迎大家改动和指正。
注意事项:
(1) 注意三类线程的创建方法和各自的线程函数的格式(返回类型各不一样)。
(2) 注意三类參数的传递:单个參数、多參数(结构体),复杂參数(类)。
(3) 採用參数传递方式进行线程间的通信仅仅适用于主线程向从线程的通信。
(4) 不知道大家看出该程序的一个BUG没有(大家能够下载project源代码,编译并执行,能够非常明显的发现。),就是线程二的随机画线线程,原意是随机画线并清除,但执行发现第一次画线时总是没有被清除掉,为什么?望大家动脑,知道的能够留言,大家一起学习和讨论!
4.消息传递方式
在Windows程序设计中,应用程序的每个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的很easy。我们能够在一个线程的运行函数中向还有一个线程发送自己定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。利用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);//使能下次计算button
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)//关闭
{//拦截关闭button消息
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;
}
执行结果:
project源代码下载地址:
http://download.csdn.net/detail/cbnotes/5007011
欢迎大家改动和指正。
注意事项:
(1) 注意线程消息的映射方式:
ON_THREAD_MESSAGE(WM_PROGRESS, &CProgressThread::OnThreadMsg)
和一般的窗体消息映射不同,不要搞错了。
(2) 注意用户界面线程的退出,计算完后要记得结束该线程,有专门的函数:
AfxEndThread(0),也能够使用PostQuitMessage(0)。这两种方式都是比較安全的。不推荐强制中止线程,否则会出现意想不到的问题。
(3) 该实例还实现了运行过程按退出button时的处理方法,採用了全局变量的
方式来控制计算线程的中途退出,我自觉得是一种比較好的控制方式。我開始採用强制中止的方式会出现消息延后的现象(就是在退出button的处理函数里向主线程发送消息postmessage),一直没有找到原因,有兴趣的朋友能够试试其他的退出方式供大家讨论学习。
(4) 注意非模式对话框退出的处理方法,和模式对话框的退出不一样的。
(5)用CreateThread()函数创建线程将返回一个线程句柄,通过该句柄你能够控制和操作该线程,当你不用时能够一创建该线程后就关闭该句柄,有专门的函数CloseHandle()。关闭句柄并代表关闭线程,仅仅是你不能在外部控制该线程(比方,提前结束,更改优先级等)。在线程结束后,系统将自己主动清理线程资源,但并不自己主动关闭该句柄,所以线程结束后要记得关闭该句柄,你可能要问为什么你这么强调要关闭该句柄,是由于该句柄(HANDLE)是内核对象,内核对象的管理是操作系统管理的,详细你能够查查相关资料,我在此就不啰嗦了。
5.线程同步法
还能够通过线程同步来实现线程间通信。比如有两个线程,线程A写入数据,线程B读出线程A准备好的数据并进行一些操作。这样的情况下,仅仅有当线程A写好数据后线程B才干读出,仅仅有线程B读出数据后线程A才干继续写入数据,这两个线程之间须要同步进行通信。关于线程同步的方法和控制是编写多线程的核心和难点,方法也比較多,在此就不详细解说和实例演示,详细内容我将在接下来的文章中一一道来,敬请继续关注我的更新,谢谢。
===========================================
转载请标明出处,谢谢。http://blog.csdn.net/cbNotes
===========================================