17.1线程
对于Windows来说所有的线程都是一样的,但MFC却把线程区分为两种类型:User Interface(UI) threads(用户界面(UI)线程)和Worker threads(工作者线程)。
两种线程的不同之处在于UI线程具有消息循环而工作者线程没有。UI线程可以创建窗口并处理发送给这些窗口的消息。工作者线程执行后台任务。
17.1.1创建工作者线程
AfxBeginThread定义了两个版本:一个启动UI线程,另一个启动过作者线程
CWinThread*pThread=AfxBeginThread(ThreadFunc,&threadInfo);
启动一个工作者线程并给它传递一个应用程序定义的数据结构的地址(&threadInfo)其中包含了对线程的输入。
ThreadFunc是“线程函数”,这类函数在线程开始执行后才得以执行。
AfxBeginThread的工作者线程可以接受另外4个参数,分别用来指定线程的优先级别、堆栈尺寸、产生标志以及安全属性。函数的完整原型是:
CWinThread* AFXAPI AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
nPriority指定了线程执行的优先级别。CWinThread::SetThreadPriority可以在任何时候修改线程的优先级别。
nStackSize参数制定了线程最大的堆栈尺寸。值0默认堆栈增加到1MB大小。
dwCreateFlags默认值0高速系统立即开始执行现成。如果指定了CREATE_SUSPENDED线程开始时就处于暂停状态。知道另一个线程(通常是创建它的线程)在展厅的线程上调用了CWinThread::ResumeThread之后才会继续执行。
例如:
CWinThread *pThread=AfxBeginThread(ThreadFunc,&threadInfo,THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDEND);
.......
pThread->ResumeThread();//Start the thread
最后一个参数lpSecurityAttrs是指向SECURTTY_ATTRIBUTES结构的指针,该结构制定了新线程的安全属性,并告诉系统自进程是否继承了线程句柄。默认值NULL意味这新线程与创建它的线程具有相同的属性。
线程函数
线程函数是回调函数,因此它必须是静态成员函数或是在类外部声明的全局函数。其原型如下
UINT ThreadFunc(LPVOID pParam);
对于两个以上的线程使用线程函数是合法的,但是应该小心由于全局变量和静态函数照成的重入问题,只要线程使用的变量和对象时在堆栈上创建的,就不会发生重入问题。
注:回调函数:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方法直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
重入:可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
17.1.2创建UI线程
创建UI线程与创建工作者线程具有截然不同的过程。工作者线程是有线程函数定义的,而UI线程的行为却是从CWinThread派生来的可动态创建类控制的,该类与从CWinAp派生的应用程序类很相似。
class CMainWindow:public CFrameWnd { public:CMainWindow(); protected: afx_msg void OnLButtonDown(UINT,CPoint); DECLARE_MESSAGE_MAP() }; BEGIN_MESSAGE_MAP(CMainWindow,CFrameWnd) ON_WM_LBUTTONDOWN() END_MESSAGE_MAP() CMainWindow::CMainWindow() { Create(NULL,_T("UI Thread Window")); } void CMainWindow::OnLButtonDown(UINT nFlags,CPoint point) { PostMessage(WM_CLOSE,0,0); } //The CUIThread class class CUIThread:public CWinThread { DECLARE_DYNCREATE(CUIThread) public: virtual BOOL InitInstance(); }; IMPLEMENT_DYNCREATE(CUIThread,CWinThread) BOOL CUIThread::InitInstance() { m_pMainWnd=new CMainWindow; m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow(); return TRUE; }
CWinThread* pThread=AfxBeginThread(RUNTIME_CLASS(CUIThread));
17.1.3 暂停和继续执行线程
运行中的线程可以用CWinThread::SuspendThread暂停,再用CWinThread:;ResumeThread继续执行。
对每一个线程,系统都维持着一个暂停数,其值有SuspendThread加1,ResumeThread减1.只有在其栈顶数为0时,才会给线程调度处理器时间。
没有CREATE_SUSPENDED标志创建的线程暂停数为0,用CREATE_SUSPENDED标志创建的线程开始就有暂停数1。
SuspendThread和ResumeThread都返回线程以前的暂停数,这样您就可以确保线程被继续执行了,无论暂停数多达都可以反复调用ResumeThread直到期返回值为1.如果当前线程没有被暂停,ResumeThread返回值为0。
17.1.4使用线程睡眠
::Sleep(0)用来放弃剩余的线程时间片。
::Sleep的值并不能保证线程在指定的间隔时间过去后的某个精确时刻醒过来。给::Sleep传递一个10000的值只能保证线程将在10秒过后的某个时刻醒转。这要取决于操作系统。
17.1.5终止线程
线程开始执行后,有两种方式可以终止它。当线程函数执行return语句是,或是此线程中任何地方的任何函数调用AfxEndThread时,工作者线程就会结束。
当给其消息队列发送了一个WM_QUIT消息或者线程自己调用了AfxEndThread时,UI线程就会结束。使用API函数::PostQuitMessage,UI线程可以给自己发送一个WM_QUIT消息。
AfxEndThread\::PostQuitMessage和return都接收一个32位的出口代码,在i型按成结束后可以用GetExitCodeThread检索到。
DWORD dwExitCode;
::GetExitCodeThread(pThread->m_hThread,&dwExitCode);
如果对正在执行的线程调用了该函数dwExitCode设置为STILL_ACTIVE(0x103)
17.1.6自动删除CWinThread
上面终止线程的语句会出现事故,因为关闭了的线程会删除CWinThread对象
有两种解决方法,第一种是设置m_bAutoDelete数据成员为FALSE可防止MFC删除CWinThread对象。默认值是TRUE允许自动删除。如果用这种方法,记住要通过AfxBeginThread返回的CWinThread指针来调用delete,否则您的应用程序就可能因内存不足而无法运行。
CWinThread *pThread= AfxBeginThread(ThreadFunc,NULL, THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED); pThread->m_bAutoDelete=FALSE; pThread->ResumeThread(); //Sometime later DWORD dwExitCode; ::GetExitCodeThread(pThread->m_hThread,&dwExitCode); if(dwExitCode==STILL_ACTIVE){ } else{ delete pThread; }
第二个解决办法是允许CWinThread执行自动删除,但要使用Win32::DuplicateHandle函数创建一个县城句柄的复件。线程疾病要进行引用计数,使用::DuplicateHandle复制一个新打开的线程句柄会将引用数从1增加到2.
所以当CWinThread的析构函数调用::CloseHandle时,句柄实际上并没有被关闭;仅仅是递减了他的引用计数。这种方法缺点是必须亲自调用CloseHandle来关闭句柄。
CWinThread *pThread= AfxBeginThread(ThreadFunc,NULL, THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED); HANDLE hThread; ::DuplicateHandle(GetCurrentProcess(),pThread->m_hThread, GetCurrentProcess(),&hThread,0,FALSE,DUPLICATE_SAME_ACCESS); pThread->ResumeThread(); //Sometime later DWORD dwExitCode; ::GetExitCodeThread(pThread->m_hThread,&dwExitCode); if(dwExitCode==STILL_ACTIVE){ } else{ ::CloseHandle(hThread); }
17.1.7结束另一个线程
下面给出一种实现此目的比较合适的方法:
//Thread A nContinue = 1; CWinThread *pThread=AfxBeginThread(ThreadFunc,&nContinue); . . . HANDLE hThread=pThread -> m_hThread;//Save the thread handle; nContinue=0;//Tell thread B to terminate. ::WaitForSingleObject(hThread,INFINITE); //Thread B UINT ThreadFunc(LPOVID pParam) { int *pContinue = (int *)pParam; while(*pContinue){ //work work work } return 0; }
::WaitForSingleObject阻止调用线程知道指定的对象(在本例中式另一线程)进入“信号发出”状态。一个线程在结束后就会处于信号发出状态。
::WaitForSingleObject会在指定的时间用完之后返回,即使对象还没进入信号发出状态。您可以通过检查返回值来确定函数返回的原因。WAIT_OBJECT_0意味着
对象进入了信号发出状态,而WAIT_TIMEOUT表示还没有进入。
通过对调用::WaitForSingleObject并将其等待时间制定为0可以快速确定线程是否仍在运行。
if(::WaitForSingleObject(hThread,0)==WAIT_OBJECT_0){ //The thread no longer exists } else{ //The thread is still running. }
这种方法调用::WaitForSingleObject就不会等待而直接返回。
还有一种方法可以用来直接终止另一线程,但是只能把它当做最后一着。
::TermiinateThread(hThread,0);
结束句柄为hThread的线程并将一个推出代码0付给他。Win32 API参考文献中列出了一些::TerminateThread可能造成的问题,从孤立线程同步对象到无法正常结束的DLL。
17.1.8线程、进程的优先级别
在任何时刻,每个线程都分配了一个从0到31的优先级。如果优先级为11的现车过你正在等待执行,而所有其他京城CPUshijian的县城具有的优先级都为10或更小,那么下一个
执行的就是优先级为11的现车过你。如果优两个优先级都为11的县城在等待执行,调度程序将执行最近最少执行的一个。
按照规律,调度程序总是将时间片分配给等待现车过你中具有最高优先级别的线程。
只要高优先级别的工作者线程不垄断CPU,即使是具有最低优先级别的线程也会得到他们所需要的全部时间(工作者线程从来不会暂停在消息队列上,因为它们根本不处理消息)。
最初的优先级分配:
当您调用AfxBeginThread或CWinThread::SetThreadPriority时,要指定“相对线程优先级”。操作系统会结合相对优先级和拥有线程的进程优先级计算出线程的“基本优先级”、实际运行
中线程的优先级(编号从0到30)由于被提高或取消提高,所以在不同时刻也不同。虽然不能提高线程的优先级,但您可以通过设置进程的优先级类型和相对线程优先级来控制基本优先级别。
进程优先级别类型
大多数进程开始时都具有优先类型NORMAL_PRIORITY_CLASS.但是一旦启动,进程就可以调用::SetPriorityClass来修改它的优先级,该函数接收进程句柄(可用::GetCurrentProcess获得)
和表17-1给出的参数。
表17-1 进程优先级别类型
优先级别类型 | 说明 |
IDLE_PRIORITY_CLASS | 只有在系统处于空闲时进程才运行,例如:对于给定的CPU没有其他线程正在等待时 |
NORMAL_PRIORITY_CLASS | 默认的进程优先级别类型。进程不需要特殊的调度。 |
HIGH_PRIORITY_CLASS | 进程接收的优先级别在IDLE_PRIORITY_CLASS和NORMAL_PRIORITY_CLASS之上 |
REALTIME_PRIORITY_CLASS | 进程必须具有可能的最高优先级,他的线程应该比甚至是属于HIGH_PRIORITY_CLASS进程的线程具有更高的优先级 |
大多数应用程序不需要修改他们的优先级类型。HIGH_PRIORITY_CLASS和REALTIME_PRIORITY_CLASS进程会极大地一直系统的响应能力,甚至会延迟关键的系统行为,如清除磁盘高速缓冲区。
HIGH_PRIORITY_CLASS的一个合法的用途是用于系统应用程序,大部分时间它都隐藏起来,只有当某种输入时间发生时它才弹出一个窗口。这些应用程序在它们暂停等待输入时值占用系统极少的额外
开销,但是一旦有某种输入出现,它们就会获得比一般应用程序高的优先级。REALTIME_PRIORITY_CLASS主要是胃实时数据获取程序停工的,为了能够使得第工作它们必须共享CPU时间。
IDLE_PRIORITY_CLASS很适合与屏幕保护、系统监视以及其他低级应用程序,它们主要用来在后台执行一些不引人注目的操作。
线程相对优先级
表17-2给出了相对线程优先级的值,可以把它们传递给AfxBeginThread和CWinThread::SetThreadPriority.
表17-2
优先级的值 | 说明 |
THREAD_PRIORITY_IDLE | 如果进程的优先级类型为HIGH_PRIORITY_CLASS或更低,则线程的基本优先级就为1,如果进程的优先级为REALTIME_PRIORITY_CLASS,则基本优先级就为16. |
THREAD_PRIORITY_LOWEST | 线程的基本优先级等于进程的优先级类型减2 |
THREAD_PRIORITY_BELOW_NORMAL | 线程的基本优先级等于进程的优先级类型减1 |
THREAD_PRIORITY_NORMAL | 默认的线程优先级值。线程的基本优先级等于进程的优先级类型 |
THREAD_PRIORITY_ABOVE_NORMAL | 线程的基本优先级等于进程的优先级类型加1 |
THREAD_PRIORITY_HIGHEST | 线程的基本优先级等于进程的优先级类型加2 |
THREAD_PRIORITY_TIME_CRITICAL | 如果进程的优先级类型为HIGH_PRIORITY_CLASS或更低,则线程的优先级就为15,如果进程的优先级类型为REALTIME_PRIORITY_CLASS,则基本优先级别就为31. |
一般规律是,如果要求高的优先级,那么利用通常是明确的。如果要求高优先级的理由不明确,那么使用普通的线程优先级就可以。对于大多数线程,默认THREAD_PRIORITY_NORMAL就祖国了。
但是如果您正在编写一个应用程序,它使用专门的线程读取和缓冲串行端口进入数据,则除非读取和缓冲线程的相对优先级别值为THREAD_PRIORITY_HIGHEST或THREAD_PRIORITY_TIME_CRITICAL,否则它将时不时地丢失字节。
17.1.9在多线程应用程序中使用C运行时函数
C和C++编译器都具有两种运行时库版本:一种是线程安全性的(可以被两个以上的线程调用)而另一种不是。运行时库的线程安全性版本通常不依赖与线程的同步化对象。相反,他在每个线程的数据结构中保存中间结果。
VisualC++带有6中不同的C运行时库版本。选用它们的依据是:正在编译的程序是进行调试状态下的创建还是发布版本的创建;希望是静态链接C运行时库还是动态链接;以及应用程序是单线程还是多线程的。表17-3给出了库的名称和相应
编译器选择开关。
表17-3 VISUAL C++中C运行时库版本
库名称 | 应用程序类型 | 选择开关 |
Libc.lib | 单线程;静态链接;发布版本的创建 | /ML |
Libcd.lib | 单线程;静态链接;调试状态下创建 | /MLd |
Libcmt.lib | 多线程;静态链接;发布版本的创建 | /MT |
Licbmtd.lib | 多线程;静态链接;调试版本下创建 | /MTd |
Msvcrt.lib | 单线程或多线程;动态链接;发布版本的创建 | /MD |
Msvcrtd.lib | 单线程或多线程;动态链接;调试状态下创建 | /MDd |
如果使用的事Visual C++,只要在Project Settings 对话框中的Use Run-time Library域中选择合适的输入项,IDE就会为您添加选项开关。
17.1.10跨线程界限调用MFC成员函数
编写多线程MFC应用程序的坏消息。只要线程不调用其他线程创建的对象的成员函数,就几乎不存在对他们执行操作的限制。会出现各种匪夷所思的情况。避免这些最基本的要求是在调用由其他线程创建的MFC对象中的成员函数之前,您必须理解他们
的隐含意思。另外要记住MFC并不是线程安全的。因此即使一个成员函数看上去是线程安全的,也要问问自己如果线程B访问一个由线程A创建的对象,在访问过程中线程A抢先了线程B怎么办?
在实际工作中多线程MFC应用程序趋向于将大量的用户界面工作交给主线程来完成。如果后台线程想要更新用户界面,它会将消息发送或公布给主线程,让主线程执行更新工作。
17.1.11第一个多线程应用程序
Sieve是一个基于对话框的应用程序,他使用了著名的Eratosthenes素数算法(筛选法)。来计算2和所有指定的数之间存在的素数的数量。电机Start按钮后开始执行计算,当结果数目出现在窗口的框中时结束。
#define WM_USER_THREAD_FINISHED WM_USER +0x100 UINT ThreadFunc(LPVOID pParam); int Sieve(int nMax); typedef struct tagTHREADPARMS{ int nMax; HWND hWnd; }THREADPARMS; . . . ON_MESSAGE(WM_USER_THREAD_FINISHED, &CMy17SieveDlg::OnThreadFinshed)//消息 . . . afx_msg LRESULT CMy17SieveDlg::OnThreadFinshed(WPARAM wParam, LPARAM lParam)//内部自定义消息函数 { SetDlgItemInt(IDC_RESULT,(int)wParam); GetDlgItem(IDC_START)->EnableWindow(TRUE); return 0; } void CMy17SieveDlg::OnBnClickedStart()//按键事件 { int nMax=GetDlgItemInt(IDC_MAX); if(nMax<10){ MessageBox("The number you enter must be 10 or higher"); GetDlgItem(IDC_MAX)->SetFocus(); return; } SetDlgItemText(IDC_RESULT,_T("")); GetDlgItem(IDC_START)->EnableWindow(FALSE); THREADPARMS *ptp=new THREADPARMS; ptp->nMax=nMax; ptp->hWnd=m_hWnd; AfxBeginThread(ThreadFunc,ptp); } //Global functions UINT ThreadFunc(LPVOID pParam)//线程全局函数 { THREADPARMS*ptp=(THREADPARMS*)pParam; int nMax=ptp->nMax; HWND hWnd=ptp->hWnd; delete ptp; int nCount =Sieve(nMax); ::PostMessage(hWnd,WM_USER_THREAD_FINISHED,(WPARAM)nCount,0); return 0; } int Sieve(int nMax)//筛选法函数 { PBYTE pBuffer =new BYTE[nMax +1]; ::FillMemory(pBuffer,nMax+1,1); int nLimit = 2; while(nLimit *nLimit<nMax) nLimit++; for(int i=2;i<=nLimit;i++) { if(pBuffer[i]){ for(int k=i+i;k<=nMax;k+=i) pBuffer[k]=0; } } int nCount =0; for(int i=2;i<=nMax;i++) if(pBuffer[i]) nCount++; delete[]pBuffer; return nCount; }