如何写优雅的代码(4)——简单有效地玩转线程

 

//========================================================================
//TITLE:
//    如何写优雅的代码(4)——简单有效地玩转线程
//AUTHOR:
//    norains
//DATE:
//    Monday 23- November-2009
//Environment:
//    WINDOWS CE 5.0
//========================================================================
    线程的使用,说复杂吧,却又是只有那几个函数,无非就是通过CreateThread创建线程,然后再通过CloseHanle关闭句柄,大不了再加一个SetThreadPriority来设置优先级;说它简单吧,如何正常退出线程,如何有效地使用线程,却又往往让初学者头疼。

 

    本文主题是如何简单却又有效地使用线程,但不涉及复杂的线程间数据交换。

 

    首先,我们先来了解如何创建线程。很简单,调用CreateThread函数即可。该函数的原型如下:

view plain copy to clipboard print ?
  1. HANDLE CreateThread(  
  2.   LPSECURITY_ATTRIBUTES lpsa,   
  3.   DWORD cbStack,   
  4.   LPTHREAD_START_ROUTINE lpStartAddr,   
  5.   LPVOID lpvThreadParam,   
  6.   DWORD fdwCreate,   
  7.   LPDWORD lpIDThread  
  8. )  
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 ?
  1. 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 ?
  1. CloseHandle(hThrd);  
CloseHandle(hThrd);

 

    需要注意的是,这里的关闭句柄,并不意味着是关闭线程处理函数,而只是将句柄对象从系统中删除而已。简单但又不失严谨来说,对于系统,有一个列表,是用来记录创建的线程对象;当该对象不再使用时,我们必须将其关闭,以避免句柄泄漏。

 

    接下来,我们再看看线程处理函数的原型:

view plain copy to clipboard print ?
  1. DWORD ThreadProc(  
  2.   LPVOID lpParameter  
  3. );  
DWORD ThreadProc( LPVOID lpParameter );

 

    没什么特别的,返回值为DWORD,形参只有一个,为lpParameter。这个lpParameter的数值,就是CreateThread的第四个形参。

我们简单地说说这形参是怎么传递的。

 

    如果我们代码是这么创建线程的:

view plain copy to clipboard print ?
  1. DWORD dwValue = 123;  
  2. 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 ?
  1. DWORD ThreadProc(LPVOID pParam)  
  2. {  
  3.   DWORD dwValue = reinterpret_cast<DWORD>(pParam);  
  4.   return 0;  
  5. }  
DWORD ThreadProc(LPVOID pParam) { DWORD dwValue = reinterpret_cast<DWORD>(pParam); return 0; }

 

    此时ThreadProc中的数值即为123。

 

    似乎这形参并没有多大的作用,如果仅仅是为了传递一个DWORD类型的数值,我们完全可以采用全局变量的方式。那我们现在将话题往前推一点,看看在类中封装线程处理函数的情形。这时候,这个看似没多大用的形参,却是我们访问成员变量或函数的唯一桥梁。

 

    在CreateThread的描述中,很清楚知道,我们不能将对象函数的地址作为参数传递,而只能传递类函数。通俗点来说,只有用了static修饰的函数才能作为形参。

 

如:

view plain copy to clipboard print ?
  1. class CBase  
  2. {  
  3. public:  
  4. DWORD ThreadProc(LPVOID pParam);  
  5. };  
class CBase { public: DWORD ThreadProc(LPVOID pParam); };

 

    上面的的ThreadProc是无法作为函数形参的。但下面的这个,就能作为形参:

view plain copy to clipboard print ?
  1. class CBase  
  2. {  
  3. public:  
  4. static DWORD ThreadProc(LPVOID pParam);  
  5. };  
class CBase { public: static DWORD ThreadProc(LPVOID pParam); };

 

    虽然增加static修饰是可以作为形参传递,但我们不可避免会遇到一个问题,就是在ThreadProc中无法访问对象成员或对象函数。解决这个问题也是很简单,我们只需要将this指针作为参数传递给ThreadProc函数,然后再转换为对象指针,就能正常访问对象成员了。

 

    创建线程时:

view plain copy to clipboard print ?
  1. HANDLE hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL);  
  2. CloseHandle(hThrd);  
HANDLE hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL); CloseHandle(hThrd);

 

    然后是线程处理函数:

view plain copy to clipboard print ?
  1. DWORD CBase::ThreadProc(LPVOID pParam)  
  2. {  
  3.  CBase *pObj = reinterpret_cast<CBase *>(pParam);  
  4.  if(pObj == NULL)  
  5. {  
  6.  ASSERT(FALSE);  
  7.  return 0x10;  
  8. }  
  9.   
  10. pObj->CheckSum(); //这里就可以直接调用对象函数   
  11. }  
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 ?
  1. DWORD CBase::ThreadProc(LPVOID pParam)  
  2. {  
  3.  //TODO:Do thing.   
  4.  ...  
  5.  return 0;  
  6. }  
DWORD CBase::ThreadProc(LPVOID pParam) { //TODO:Do thing. ... return 0; }

 

    也许有人会问,API不是有TerminateThread函数么,调用该函数为什么不可以?当然可以,只不过非常不好。

 

    加入我们有一个线程函数的代码如下:

view plain copy to clipboard print ?
  1. DWORD CBase::ThreadProc(LPVOID pParam)  
  2. {  
  3. LABEL1:  
  4.  if(IsTimeOut() == FALSE)  
  5.  {  
  6.   g_iFlag |= MUTEX_NOP;  
  7. }  
  8.   
  9. LABEL2:  
  10. if(IsCheckSystem() == FALSE)  
  11.  {  
  12.   g_iFlag |= MUTEX_CHECK;  
  13. }  
  14.   
  15. LABEL3:  
  16. if(IsBeautiful() == FALSE)  
  17.  {  
  18.   g_iFlag |= BEAUTIFULE;  
  19. }  
  20.   
  21.  return 0;  
  22. }  
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 ?
  1. void CBase::Create()  
  2. {  
  3.  //创建事件   
  4.  m_hEventWait = CreateEvent(NULL,FALSE,FALSE,TEXT(“EVENT_WAIT”));  
  5.   
  6.  //创建线程   
  7.  m_hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL);  
  8. }  
  9.   
  10. DWORD CBase::ThreadProc(LPVOID pParam)  
  11. {  
  12. CBase *pObj = reinterpret_cast< CBase *>(pParam);  
  13.    if(pObj == NULL)  
  14.    {  
  15.   return 0x10;  
  16. }  
  17.   
  18. while(TRUE)  
  19. {   
  20. //等待事件   
  21. WaitForSingleObject(pObj->m_hEvnetWait, INFINITE);  
  22.   
  23. //TODO:在这里做接收到事件的动作   
  24. }  
  25. }  
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 ?
  1. DWORD CBase::ThreadProc(LPVOID pParam)  
  2. {  
  3. CBase *pObj = reinterpret_cast< CBase *>(pParam);  
  4.   if(pObj == NULL)  
  5.   {  
  6.    return 0x10;  
  7. }  
  8.   
  9. while(pObj->m_ExitProc != FALSE)  
  10. {  
  11. //每隔100MS就从函数返回,然后判断是否需要线程退出   
  12. if(WaitForSingleObject(pObj->m_hEvnetWait, 100) != WAIT_TIMEOUT)  
  13. {  
  14. //TODO:在这里做接收到事件的动作   
  15. }  
  16. }  
  17. }  
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 ?
  1. void CBase::Create()  
  2. {  
  3. //创建唤醒事件   
  4.  m_hEvent[0] = CreateEvent(NULL,FALSE,FALSE,NULL);  
  5.   
  6.  //创建等待事件   
  7.  m_hEvent[1] = CreateEvent(NULL,FALSE,FALSE,TEXT(“EVENT_WAIT”));  
  8.   
  9.  //创建线程   
  10.  m_hThrd = CreateThread(NULL,NULL,ThreadProc,this,NULL,NULL);  
  11. }  
  12.   
  13.   
  14. DWORD CBase::ThreadProc(LPVOID pParam)  
  15. {  
  16. CBase *pObj = reinterpret_cast< CBase *>(pParam);  
  17.   if(pObj == NULL)  
  18.   {  
  19.    return 0x10;  
  20. }  
  21.   
  22. while(TRUE)  
  23. {  
  24. //等待多个事件   
  25. DWORD dwObj = WaitForMultipleObjects (pObj->m_hEvnetWait, INFINITE);  
  26.   
  27. if(dwObj == WAIT_OBJECT_0)  
  28. {  
  29.  //跳出循环,退出函数   
  30.  break;  
  31. }  
  32. else  
  33. {  
  34. //TODO:在这里做接收到事件的动作   
  35. }  
  36. }  
  37. }  
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 ?
  1. DWORD CBase::ThreadProc(LPVOID pParam)  
  2. {  
  3. CBase *pObj = reinterpret_cast< CBase *>(pParam);  
  4.   if(pObj == NULL)  
  5.   {  
  6.    return 0x10;  
  7. }  
  8.   
  9. //在这里设置线程运行标识   
  10.  InterlockedExchange(reinterpret_cast<LONG *>(&m_bThrdRunning),TRUE);  
  11.   
  12. while(TRUE)  
  13. {  
  14. //等待多个事件   
  15. DWORD dwObj = WaitForMultipleObjects (pObj->m_hEvnetWait, INFINITE);  
  16.   
  17. if(dwObj == WAIT_OBJECT_0)  
  18. {  
  19.  //跳出循环,退出函数   
  20.  break;  
  21. }  
  22. else  
  23. {  
  24. //TODO:在这里做接收到事件的动作   
  25. }  
  26. }  
  27.   
  28. //在这里设置线程退出标识   
  29. InterlockedExchange(reinterpret_cast<LONG *>(&m_bThrdRunning),FALSE);  
  30. return 0;  
  31. }  
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 ?
  1. void CBase::Close(CloseFlag flag)  
  2. {   
  3.  if(m_bProcRunning != FALSE)  
  4.  {    
  5.   SetEvent(m_hEvent[0]);  
  6.   
  7.   if(flag == CLOSE_ASYNC)  
  8.   {  
  9.    //为异步模式,世界返回   
  10.    return;  
  11.   }  
  12.   
  13.   //自旋等待,直到其线程退出   
  14.   while(InterlockedExchange(reinterpret_cast<LONG *>(& m_bThrdRunning),TRUE) == TRUE)  
  15.   {  
  16.    SetEvent(m_hEvent[0]);  
  17.    Sleep(100);  
  18.   }  
  19.   InterlockedExchange(reinterpret_cast<LONG *>(& m_bThrdRunning),FALSE);  
  20.  }   
  21.   
  22.  //关闭线程句柄   
  23.  CloseHandle(m_hThrd);  
  24.  m_hThrd = NULL;  
  25. }  
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; }

你可能感兴趣的:(如何写优雅的代码(4)——简单有效地玩转线程)