//========================================================================
//TITLE:
// 如何写优雅的代码(4)——简单有效地玩转线程
//AUTHOR:
// norains
//DATE:
// Monday 23- November-2009
//Environment:
// WINDOWS CE 5.0
//========================================================================
线程的使用,说复杂吧,却又是只有那几个函数,无非就是通过CreateThread创建线程,然后再通过CloseHanle关闭句柄,大不了再加一个SetThreadPriority来设置优先级;说它简单吧,如何正常退出线程,如何有效地使用线程,却又往往让初学者头疼。
本文主题是如何简单却又有效地使用线程,但不涉及复杂的线程间数据交换。
首先,我们先来了解如何创建线程。很简单,调用CreateThread函数即可。该函数的原型如下:
view plain copy to clipboard print ?
- HANDLE CreateThread(
- LPSECURITY_ATTRIBUTES lpsa,
- DWORD cbStack,
- LPTHREAD_START_ROUTINE lpStartAddr,
- LPVOID lpvThreadParam,
- DWORD fdwCreate,
- LPDWORD lpIDThread
- )
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParam, DWORD fdwCreate, LPDWORD lpIDThread )
lpsa形参好办,不用我们担心,只要直接设置为NULL;cbStack也容易,一般使用的话,我们也很少使用自定义的堆栈,所以这个也可以直接这只为NULL。lpStartAddr是最重要的一个,指向我们线程的处理函数。lpvThreadParam是传递给处理函数的形参,如果线程处理函数是封装于类中,那么这玩意可万万不能忽略。后面的fdwCreate和lpIDThread,如果没有特殊的用途,也可以一并设置为NULL。
所以,最简单的的线程创建函数的调用将可以如此:
view plain copy to clipboard print ?
- HANDLE hThrd = CreateThread(NULL,NULL,StartAddr,NULL,NULL,NULL);
HANDLE hThrd = CreateThread(NULL,NULL,StartAddr,NULL,NULL,NULL);
如果后续不需要对该线程进行设置,比如更改优先级之类,那么创建完毕后,我们可以调用CloseHandle关闭句柄:
view plain copy to clipboard print ?
- CloseHandle(hThrd);
CloseHandle(hThrd);
需要注意的是,这里的关闭句柄,并不意味着是关闭线程处理函数,而只是将句柄对象从系统中删除而已。简单但又不失严谨来说,对于系统,有一个列表,是用来记录创建的线程对象;当该对象不再使用时,我们必须将其关闭,以避免句柄泄漏。
接下来,我们再看看线程处理函数的原型:
view plain copy to clipboard print ?
- DWORD ThreadProc(
- LPVOID lpParameter
- );
DWORD ThreadProc( LPVOID lpParameter );
没什么特别的,返回值为DWORD,形参只有一个,为lpParameter。这个lpParameter的数值,就是CreateThread的第四个形参。
我们简单地说说这形参是怎么传递的。
如果我们代码是这么创建线程的:
view plain copy to clipboard print ?
- DWORD dwValue = 123;
- CreateThread(NULL,NULL,ThreadProc,reinterpret_cast<VOID *>(dwValue),NULL,NULL);
DWORD dwValue = 123; CreateThread(NULL,NULL,ThreadProc,reinterpret_cast<VOID *>(dwValue),NULL,NULL);
那么我们的线程可以这么获取数值:
view plain copy to clipboard print ?
- DWORD ThreadProc(LPVOID pParam)
- {
- DWORD dwValue = reinterpret_cast<DWORD>(pParam);
- return 0;
- }
DWORD ThreadProc(LPVOID pParam) { DWORD dwValue = reinterpret_cast<DWORD>(pParam); return 0; }
此时ThreadProc中的数值即为123。
似乎这形参并没有多大的作用,如果仅仅是为了传递一个DWORD类型的数值,我们完全可以采用全局变量的方式。那我们现在将话题往前推一点,看看在类中封装线程处理函数的情形。这时候,这个看似没多大用的形参,却是我们访问成员变量或函数的唯一桥梁。
在CreateThread的描述中,很清楚知道,我们不能将对象函数的地址作为参数传递,而只能传递类函数。通俗点来说,只有用了static修饰的函数才能作为形参。
如:
view plain copy to clipboard print ?
- class CBase
- {
- public:
- DWORD ThreadProc(LPVOID pParam);
- };
class CBase { public: DWORD ThreadProc(LPVOID pParam); };
上面的的ThreadProc是无法作为函数形参的。但下面的这个,就能作为形参:
view plain copy to clipboard print ?
- class CBase
- {
- public:
- static DWORD ThreadProc(LPVOID pParam);
- };
class CBase { public: static DWORD ThreadProc(LPVOID pParam); };
虽然增加static修饰是可以作为形参传递,但我们不可避免会遇到一个问题,就是在ThreadProc中无法访问对象成员或对象函数。解决这个问题也是很简单,我们只需要将this指针作为参数传递给ThreadProc函数,然后再转换为对象指针,就能正常访问对象成员了。
创建线程时:
view plain copy to clipboard print ?
- HANDLE hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL);
- CloseHandle(hThrd);
HANDLE hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL); CloseHandle(hThrd);
然后是线程处理函数:
view plain copy to clipboard print ?
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
- CBase *pObj = reinterpret_cast<CBase *>(pParam);
- if(pObj == NULL)
- {
- ASSERT(FALSE);
- return 0x10;
- }
-
- pObj->CheckSum();
- }
DWORD CBase::ThreadProc(LPVOID pParam) { CBase *pObj = reinterpret_cast<CBase *>(pParam); if(pObj == NULL) { ASSERT(FALSE); return 0x10; } pObj->CheckSum(); //这里就可以直接调用对象函数 }
在类中封装线程函数就是这么简单,关键只在于传递this指针而已。线程的基础差不多就说到这里,如果需要更详细的说明,可以查阅相关文档。只不过,到目前为止的介绍,对于接下来的说明已经足够了。
为了方便,接下来的讨论,我们都假设所有的操作都封装在类里。
之前我们有讨论过,CloseHandle并不是关闭线程,只是将线程的句柄从系统的列表中删除,那么,我们应该如何关闭线程呢?
普遍的,也是最受推荐的,就是让线程自己返回。
比如:
view plain copy to clipboard print ?
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
-
- ...
- return 0;
- }
DWORD CBase::ThreadProc(LPVOID pParam) { //TODO:Do thing. ... return 0; }
也许有人会问,API不是有TerminateThread函数么,调用该函数为什么不可以?当然可以,只不过非常不好。
加入我们有一个线程函数的代码如下:
view plain copy to clipboard print ?
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
- LABEL1:
- if(IsTimeOut() == FALSE)
- {
- g_iFlag |= MUTEX_NOP;
- }
-
- LABEL2:
- if(IsCheckSystem() == FALSE)
- {
- g_iFlag |= MUTEX_CHECK;
- }
-
- LABEL3:
- if(IsBeautiful() == FALSE)
- {
- g_iFlag |= BEAUTIFULE;
- }
-
- return 0;
- }
DWORD CBase::ThreadProc(LPVOID pParam) { LABEL1: if(IsTimeOut() == FALSE) { g_iFlag |= MUTEX_NOP; } LABEL2: if(IsCheckSystem() == FALSE) { g_iFlag |= MUTEX_CHECK; } LABEL3: if(IsBeautiful() == FALSE) { g_iFlag |= BEAUTIFULE; } return 0; }
如果在线程函数还在执行的时候,就调用TerminateThread,那么,最终g_iFlag会是什么数值?
如果执行到LABEL1,刚好调用TerminateThread,那么g_iFlag等于原值;如果是刚好执行到LABEL2,那么g_iFlag会设置MUTEX_CHECK位;如果再往下执行到LABEL3,那么就又和之前的完全不同。
更为重要的是,多线程,你在调用Terminate时,根本无法知道ThreadProc究竟执行到了哪一步。换句话说,这程序,每次实行,都可能会和上一次不一样,这难道不是一个灾难么?
所以,还是老老实实,线程该咋样就咋样,该自己退出就让它自生自灭吧!
线程的使用多种多样,本文无法一一列举,因此接下来的讨论,我们将范围缩小,局限于线程是不停地循环接收事件。
根据该要求我们很简单地罗列出相应的代码:
view plain copy to clipboard print ?
- void CBase::Create()
- {
-
- m_hEventWait = CreateEvent(NULL,FALSE,FALSE,TEXT(“EVENT_WAIT”));
-
-
- m_hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL);
- }
-
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
- CBase *pObj = reinterpret_cast< CBase *>(pParam);
- if(pObj == NULL)
- {
- return 0x10;
- }
-
- while(TRUE)
- {
-
- WaitForSingleObject(pObj->m_hEvnetWait, INFINITE);
-
-
- }
- }
void CBase::Create() { //创建事件 m_hEventWait = CreateEvent(NULL,FALSE,FALSE,TEXT(“EVENT_WAIT”)); //创建线程 m_hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL); } DWORD CBase::ThreadProc(LPVOID pParam) { CBase *pObj = reinterpret_cast< CBase *>(pParam); if(pObj == NULL) { return 0x10; } while(TRUE) { //等待事件 WaitForSingleObject(pObj->m_hEvnetWait, INFINITE); //TODO:在这里做接收到事件的动作 } }
不过这段代码确实是有问题,因为我们无法让线程自己退出。那么,我们先采用一个最简单的方式,设置一个标志位,当该标志位为TRUE时,我们让线程跳出循环,然后直接线程返回。
线程部分代码更改如下:
view plain copy to clipboard print ?
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
- CBase *pObj = reinterpret_cast< CBase *>(pParam);
- if(pObj == NULL)
- {
- return 0x10;
- }
-
- while(pObj->m_ExitProc != FALSE)
- {
-
- if(WaitForSingleObject(pObj->m_hEvnetWait, 100) != WAIT_TIMEOUT)
- {
-
- }
- }
- }
DWORD CBase::ThreadProc(LPVOID pParam) { CBase *pObj = reinterpret_cast< CBase *>(pParam); if(pObj == NULL) { return 0x10; } while(pObj->m_ExitProc != FALSE) { //每隔100MS就从函数返回,然后判断是否需要线程退出 if(WaitForSingleObject(pObj->m_hEvnetWait, 100) != WAIT_TIMEOUT) { //TODO:在这里做接收到事件的动作 } } }
但这样的修改,其实在效率上还是有点问题的。因为我们需要判断m_ExitProc的数值,所以我们对于WaitForSingleObject需要每隔一段时间就从等待中返回,然后再判断标志位。在这间隔性的返回当中,我们白白耗费了不少CPU时间。
为了避免这种无谓的损耗,我们应该改用WaitForMultipleObjects函数,同时等待两个事件。其中一个事件当然是我们之前所需要的,另外一个新的事件我们称其为唤醒事件,当接收到该事件时,我们就直接退出线程。
根据这个思想,那么我们代码又可以改装如下:
view plain copy to clipboard print ?
- void CBase::Create()
- {
-
- m_hEvent[0] = CreateEvent(NULL,FALSE,FALSE,NULL);
-
-
- m_hEvent[1] = CreateEvent(NULL,FALSE,FALSE,TEXT(“EVENT_WAIT”));
-
-
- m_hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL);
- }
-
-
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
- CBase *pObj = reinterpret_cast< CBase *>(pParam);
- if(pObj == NULL)
- {
- return 0x10;
- }
-
- while(TRUE)
- {
-
- DWORD dwObj = WaitForMultipleObjects (pObj->m_hEvnetWait, INFINITE);
-
- if(dwObj == WAIT_OBJECT_0)
- {
-
- break;
- }
- else
- {
-
- }
- }
- }
void CBase::Create() { //创建唤醒事件 m_hEvent[0] = CreateEvent(NULL,FALSE,FALSE,NULL); //创建等待事件 m_hEvent[1] = CreateEvent(NULL,FALSE,FALSE,TEXT(“EVENT_WAIT”)); //创建线程 m_hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL); } DWORD CBase::ThreadProc(LPVOID pParam) { CBase *pObj = reinterpret_cast< CBase *>(pParam); if(pObj == NULL) { return 0x10; } while(TRUE) { //等待多个事件 DWORD dwObj = WaitForMultipleObjects (pObj->m_hEvnetWait, INFINITE); if(dwObj == WAIT_OBJECT_0) { //跳出循环,退出函数 break; } else { //TODO:在这里做接收到事件的动作 } } }
嗯,这一下子,效率是提上去了。如果啥事情都没有呢,这线程就乖乖地在休息;如果有事情呢,它就会立马苏醒,然后再看看外面的世界。
只不过,工作正常了,并不代表优雅。简单地说,如果我们想知道当前这线程究竟是在运行,还是不在运行呢?
这个也非常简单,我们再给线程函数添加一个变量,当进入的时候设置TRUE,退出的时候设置FALSE。是不是也很简单呢?
代码如下:
view plain copy to clipboard print ?
- DWORD CBase::ThreadProc(LPVOID pParam)
- {
- CBase *pObj = reinterpret_cast< CBase *>(pParam);
- if(pObj == NULL)
- {
- return 0x10;
- }
-
-
- InterlockedExchange(reinterpret_cast<LONG *>(&m_bThrdRunning),TRUE);
-
- while(TRUE)
- {
-
- DWORD dwObj = WaitForMultipleObjects (pObj->m_hEvnetWait, INFINITE);
-
- if(dwObj == WAIT_OBJECT_0)
- {
-
- break;
- }
- else
- {
-
- }
- }
-
-
- InterlockedExchange(reinterpret_cast<LONG *>(&m_bThrdRunning),FALSE);
- return 0;
- }
DWORD CBase::ThreadProc(LPVOID pParam) { CBase *pObj = reinterpret_cast< CBase *>(pParam); if(pObj == NULL) { return 0x10; } //在这里设置线程运行标识 InterlockedExchange(reinterpret_cast<LONG *>(&m_bThrdRunning),TRUE); while(TRUE) { //等待多个事件 DWORD dwObj = WaitForMultipleObjects (pObj->m_hEvnetWait, INFINITE); if(dwObj == WAIT_OBJECT_0) { //跳出循环,退出函数 break; } else { //TODO:在这里做接收到事件的动作 } } //在这里设置线程退出标识 InterlockedExchange(reinterpret_cast<LONG *>(&m_bThrdRunning),FALSE); return 0; }
看到这里也许有人会觉得奇怪,因为对于m_bThrdRunning变量来说,也只有在线程里才会变更其数值,为什么还要祭出InterlockedExchange呢?对,没错,如果外部只需要读取其数值,而不用更改,那么只要简单地调用等号就好了。那为什么我们还要这么弄呢?主要是考虑到关闭线程的函数。
简单点来说,我们关闭线程的函数应该分为两种模式。一种模式为同步,另一种为异步。换句话来说,当其为异步模式时,我们只需要像线程发送个事件就好了;如果为同步模式,那么发送事件完毕后,我们还要判断退出标识。这时候,InterlockedExchange就派上用场了,我们可以采用它做一个自旋判断,直到其为FALSE,我们才退出关闭函数。
如上所言,则关闭函数如下:
view plain copy to clipboard print ?
- void CBase::Close(CloseFlag flag)
- {
- if(m_bProcRunning != FALSE)
- {
- SetEvent(m_hEvent[0]);
-
- if(flag == CLOSE_ASYNC)
- {
-
- return;
- }
-
-
- while(InterlockedExchange(reinterpret_cast<LONG *>(& m_bThrdRunning),TRUE) == TRUE)
- {
- SetEvent(m_hEvent[0]);
- Sleep(100);
- }
- InterlockedExchange(reinterpret_cast<LONG *>(& m_bThrdRunning),FALSE);
- }
-
-
- CloseHandle(m_hThrd);
- m_hThrd = NULL;
- }
void CBase::Close(CloseFlag flag) { if(m_bProcRunning != FALSE) { SetEvent(m_hEvent[0]); if(flag == CLOSE_ASYNC) { //为异步模式,世界返回 return; } //自旋等待,直到其线程退出 while(InterlockedExchange(reinterpret_cast<LONG *>(& m_bThrdRunning),TRUE) == TRUE) { SetEvent(m_hEvent[0]); Sleep(100); } InterlockedExchange(reinterpret_cast<LONG *>(& m_bThrdRunning),FALSE); } //关闭线程句柄 CloseHandle(m_hThrd); m_hThrd = NULL; }