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函数