多线程程序设计(二)

4.退出代码Exit Code

成员Exit Code指定了线程的退出代码,也可以说是线程函数的返回值。在线程运行期间,线程函数还没有返回,Exit Code的值是STILL_ACTIVE。线程运行结束后,系统自动将ExitCode设为线程函数的返回值。可以用GetExitCodeThread函数得到线程的退出代码。

         ……

         DWORD dwExitCode;

         if(::GetExitCodeThread(hThread, &dwExitCode))

         {       if(dwExitCode == STILL_ACTIVE)

                   {                }                          // 目标线程还在运行          

                   else

                   {                }                          // 目标线程已经中止,退出代码为dwExitCode    

         }

         ……

5.是否受信Signaled

成员Signaled指示了线程对象是否为“受信”状态。线程在运行期间,Signaled的值永远是FALSE,即“未受信”,只有当线程结束以后,系统才把Signaled的值置为TRUE。此时,针对此对象的等待函数就会返回,如上一小节中的WaitForSingleObject函数。

3.1.3 线程的终止

当线程正常终止时,会发生下列事件:

l         在线程函数中创建的所有C++对象将通过它们各自的析构函数被正确地销毁。

l         该线程使用的堆栈将被释放。

l         系统将线程内核对象中Exit Code(退出代码)的值由STILL_ACTIVE设置为线程函数的返回值。

l         系统将递减线程内核对象中Usage Code(使用计数)的值。

线程结束后的退出代码可以被其他线程用GetExitCodeThread函数检测到,所以可以当做自定义的返回值来表示线程的执行结果。终止线程的执行有4种方法。

(1)线程函数自然退出。当函数执行到return语句返回时,Windows将终止线程的执行。建议使用这种方法终止线程的执行。

(2)使用ExitThread函数来终止线程,原型如下:

void ExitThread( DWORD dwExitCode);       // 线程的退出代码

ExitThread函数会中止当前线程的运行,促使系统释放掉所有此线程使用的资源。但是,C/C++资源却不能得到正确地清除。例如,在下面一段代码中,theObject对象的析构函数就不会被调用。

class CMyClass

{

public:

         CMyClass() { printf(" Constructor\n"); }

         ~CMyClass() { printf(" Destructor\n"); }

};

void main()

{       CMyClass theObject;

         ::ExitThread(0); // ExitThread函数使线程立刻中止,theObject对象的析构函数得不到机会被调用

         // 在函数的结尾,编译器会自动添加一些必要的代码,来调用theObject的析构函数

}

运行上面的代码,将会看到程序的输出。

Constructor

一个对象被创建,但是永远也看不到Destructor这个单词出现。theObject这个C++对象没有被正确地销毁,原因是ExitThread函数强制该线程立刻终止,C/C++运行期没有机会执行清除代码。

所以结束线程最好的方法是让线程函数自然返回。如果在上面的代码中删除了对ExitThread的调用,再次运行程序产生的输出结果如下:

Constructor

Destructor

(3)使用TerminateThread函数在一个线程中强制终止另一个线程的执行,原型如下:

BOOL TerminateThread(

HANDLE hThread,           // 目标线程句柄

DWORD dwExitCode       // 目标线程的退出代码

);

这是一个被强烈建议避免使用的函数,因为一旦执行这个函数,程序无法预测目标线程会在何处被终止,其结果就是目标线程可能根本没有机会来做清除工作,比如,线程中打开的文件和申请的内存都不会被释放。另外,使用TerminateThread函数终止线程的时候,系统不会释放线程使用的堆栈。所以,建议读者在编程的时候尽量让线程自己退出。如果主线程要求某个线程结束,可以通过各种方法通知线程,线程收到通知后自行退出。只有在迫不得已的情况下,才使用TerminateThread函数终止线程。

(4)使用ExitProcess函数结束进程,这时系统会自动结束进程中所有线程的运行。用这种方法相当于对每个线程使用TerminateThread函数,所以也应当避免这种情况。

总之,始终应该让线程正常退出,即由它的线程函数返回。通知线程退出的方法很多,如使用事件对象、设置全局变量等,这是下一节的话题。

3.1.4 线程的优先级

每个线程都要被赋予一个优先级号,取值为0(最低)到31(最高)。当系统确定哪个线程需要分配CPU时,它先检查优先级为31的线程,然后以循环的方式对他们进行调度。如果有一个优先级为31的线程可调度,它就会被分配到一个CPU上运行。在该线程的时间片结束时,系统查看是否还有另一个优先级为31的线程,如果有,就安排这个线程到CPU上运行。

Windows调度线程的原则就是这样的,只要优先级为31的线程是可调度的,就绝对不会将优先级为0~30的线程分配给CPU。大家可能以为,在这样的系统中,低优先级的线程永远得不到机会运行。事实上,在任何一段时间内,系统中的线程大多是不可调度的,即处于暂停状态。比如3.1.1小节的例子中,调用WaitForSingleObject函数就会导致主线程处于不可调度状态,还有在第4章要讨论的GetMessage函数,也会使线程暂停运行。

Windows支持6个优先级类:idle、below normal、normal、above normal、high和real-time。从字面上也可以看出,normal是被绝大多数应用程序采用的优先级类。其实,进程也是有优先级的,只是在实际的开发过程中很少使用而已。进程属于一个优先级类,还可以为进程中的线程赋予一个相对线程优先级。但是,一般情况下并不改变进程的优先级(默认是nomal),所以可以认为,线程的相对优先级就是它的真实优先级,与其所在的进程的优先级类无关。

线程刚被创建时,他的相对优先级总是被设置为normal。若要改变线程的优先级,必须使用下面这个函数:

BOOL SetThreadPriority(HANDLE hThread,int nPriority );

hThread参数是目标线程的句柄,nPriority参数定义了线程的优先级,取值如下所示:

l          THREAD_PRIORITY_TIME_CRITICAL              Time-critical(实时)

l          THREAD_PRIORITY_HIGHEST                                     Highest(最高)

l          THREAD_PRIORITY_ABOVE_NORMAL           Above normal(高于正常,Windows 98不支持)

l          THREAD_PRIORITY_NORMAL                           Normal(正常)

l          THREAD_PRIORITY_BELOW_NORMAL          Below normal(低于正常,Windows 98不支持)

l          THREAD_PRIORITY_LOWEST                             Lowest(最低)

l          THREAD_PRIORITY_IDLE                                    Idle(空闲)

下面的小例子说明了优先级的不同给线程带来的影响。它同时创建了两个线程,一个线程的优先级是“空闲”,运行的时候不断打印出“Idle Thread is running”;另一个线程的优先级为“正常”,运行的时候不断打印出“Normal Thread is running”字符串。源程序代码如下:

DWORD WINAPI ThreadIdle(LPVOID lpParam)                      // 03PriorityDemo工程下

{       int i = 0;

         while(i++<10)

                   printf("Idle Thread is running \n");

         return 0;

}

DWORD WINAPI ThreadNormal(LPVOID lpParam)

{       int i = 0;

         while(i++<10)

                   printf(" Normal Thread is running \n");

         return 0;

}

int main(int argc, char* argv[])

{       DWORD dwThreadID;

         HANDLE h[2];

         // 创建一个优先级为Idle的线程

         h[0] = ::CreateThread(NULL, 0, ThreadIdle, NULL,

                                                                 CREATE_SUSPENDED, &dwThreadID);

         ::SetThreadPriority(h[0], THREAD_PRIORITY_IDLE);

         ::ResumeThread(h[0]);

         // 创建一个优先级为Normal的线程

         h[1] = ::CreateThread(NULL, 0, ThreadNormal, NULL,

                                                                 0, &dwThreadID);

         // 等待两个线程内核对象都变成受信状态

         ::WaitForMultipleObjects(

                   2,                     // DWORD nCount 要等待的内核对象的数量

                   h,                     // CONST HANDLE *lpHandles 句柄数组

                   TRUE,           // BOOL bWaitAll        指定是否等待所有内核对象变成受信状态

                   INFINITE);     // DWORD dwMilliseconds 要等待的时间

         ::CloseHandle(h[0]);

         ::CloseHandle(h[1]);

         return 0;

}

程序运行结果如图3.2所示。可以看到,只要有优先级高的线程处于可调度状态,Windows是不允许优先级相对低的线程占用CPU的。

3.2 两个优先级不同的线程

创建第一个线程时,将CREATE_SUSPENDED标记传给了CreateThread函数,这可以使新线程处于暂停状态。在将它的优先级设为THREAD_PRIORITY_IDLE后,再调用ResumeThread函数恢复线程运行。这种改变线程优先级的方法在实际编程过程中经常用到。

WaitForMultipleObjects函数用于等待多个内核对象,前两个参数分别为要等待的内核对象的个数和句柄数组指针。如果将第三个参数bWaitAll的值设为TRUE,等待的内核对象全部变成受信状态以后此函数才返回。否则,bWaitAll为0的话,只要等待的内核对象中有一个变成了受信状态,WaitForMultipleObjects就返回,返回值指明了是哪一个内核对象变成了受信状态。下面的代码说明了函数返回值的作用:

         HANDLE h[2];

         h[0] = hThread1;

         h[1] = hThread2;

         DWORD dw = ::WaitForMultipleObjects(2, h, FALSE, 5000);

         switch(dw)

         {       case WAIT_FAILED:

                           // 调用WaitForMultipleObjects函数失败(句柄无效?)

                            break;

                  case WAIT_TIMEOUT:

                           // 在5秒内没有一个内核对象受信

                            break;

                  case WAIT_OBJECT_0 + 0:

                           // 句柄h[0]对应的内核对象受信

                            break;

                  case WAIT_OBJECT_0 + 1:

                           // 句柄h[1]对应的内核对象受信

                            break;

         }

参数bWaitAll为FALSE的时候,WaitForMultipleObjects函数从索引0开始扫描整个句柄数组,第一个受信的内核对象将终止函数的等待,使函数返回。

有的时候使用高优先级的线程是非常必要的。比如,Windows Explorer进程中的线程就是在高优先级下运行的。大部分时间里,Explorer的线程都处于暂停状态,等待接受用户的输入。当Explorer的线程被挂起的时候,系统不给它们安排CPU时间片,使其他低优先级的线程占用CPU。但是,一旦用户按下一个键或组合键,例如Ctrl+Esc,系统就唤醒Explorer的线程(用户按Ctrl+Esc时,开始菜单将出现)。如果该时刻有其他优先级低的线程正在运行的话,系统会立刻挂起这些线程,允许Explorer的线程运行。这就是抢占式优先操作系统。

3.1.5 C/C++运行期库

在实际的开发过程中,一般不直接使用Windows系统提供的CreateThread函数创建线程,而是使用C/C++运行期函数_beginthreadex。本小节主要来分析一下_beginthreadex函数的内部实现。

事实上,C/C++运行期库提供另一个版本的CreateThread是为了多线程同步的需要。在标准运行库里面有许多全局变量,如errno、strerror等,它们可以用来表示线程当前的状态。但是在多线程程序设计中,每个线程必须有惟一的状态,否则这些变量记录的信息就不会准确了。比如,全局变量errno用于表示调用运行期函数失败后的错误代码。如果所有线程共享一个errno的话,在一个线程产生的错误代码就会影响到另一个线程。为了解决这个问题,每个线程都需要有自己的errno变量。

要想使运行期为每个线程都设置状态变量,必须在创建线程的时候调用运行期提供的_beginthreadex,让运行期设置了相关变量后再去调用Windows系统提供的CreateThread函数。_beginthreadex的参数与CreateThread函数是对应的,只是参数名和类型不完全相同,使用的时候需要强制转化。

unsigned long _beginthreadex(

   void *security,

   unsigned stack_size,

   unsigned ( __stdcall *start_address )( void * ),

   void *arglist,

   unsigned initflag,

   unsigned *thrdaddr

);

VC++默认的C/C++运行期库并不支持_beginthreadex函数。这是因为标准C运行期库是在1970年左右问世的,那个时候还没有多线程这个概念,也就没有考虑到将C运行期库用于多线程应用程序所出现的问题。要想使用_beginthreadex函数,必须对VC进行设置,更换它默认使用的运行期库。

选择菜单命令“Project/Settings…”,打开标题为“Project Settings”的对话框,如图3.3所示。选中C/C++选项卡,在Category对应的组合框中选择Code Generation类别。从Use run-time library组合框中选定6个选项中的一个。默认的选择是第一个,即Single-Threaded,此选项对应着单线程应用程序的静态链接库。为了使用多线程,选中Multithreaded DLL就可以了。后两节的例子就使用_beginthreadex函数来创建线程。

图3.3 选择支持多线程的运行期库

相应地,C/C++运行期库也提供了另一个版本的结束当前线程运行的函数,用于取代ExitThread函数。

void _endthreadex(unsigned retval );                 // 指定退出代码

这个函数会释放_beginthreadex为保持线程同步而申请的内存空间,然后再调用ExitThread函数来终止线程。同样,笔者还是建议让线程自然退出,而不要使用_endthreadex函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值