要想掌握Windows平台下的线程,应首先理解“内核对象”(Kernel Objects)的概念。如果仅介绍Windows平台下的线程使用技巧,则可以省略相对陌生的内核对象相关内容。但这并不能使各位深入理解Windows平台下的线程。
操作系统创建的资源有很多种,如进程、线程、文件及即将介绍的信号量、互斥量等。其中大部分都是通过程序员的请求创建的,而且请求方式各不相同。虽然存在一些差异,但它们之间也有如下共同点:“都是由Windows操作系统创建并管理的资源。”
不同资源类型在“管理”方式也有差异。例如,文件管理中应注册并更新文件相关的数据I/O位置、文件的打并模式(rcad or write)等。如果是线程,则应注册并维护线程ID、线程所属进程等信息。操作系统为了以记录相关信息的方式管理各种资源,在其内部生成数据块。当然,每种资源需要维护的信息不同,所以每种资源拥有的数据块格式也有差异。这类数据块称为“内核对象”。
假设在Windows下创建了mydata.txt文件,此时Windows操作系统将生成1个数据块以便管理,该数据块就是内核对象。同理,Windows在创建进程、线程、线程同步信号量时也会生成相应的内核对象,用于管理操作系统资源。
线程、文件等资源的创建请求均在进程内部完成,因此,很容易产生“此时创建的内核对象所有者就是进程”的错觉。其实,内核对象所有者是内核(操作系统)。“所有者是内核”具有如下含义:
“内核对象的创建、管理、销毁时机的决定等工作均由操作系统完成!"
既然在第18章学习过线程,那么请回答如下问题:“程序开始运行后,调用main函数的主体是进程还是线程?”
调用main函数的主体是线程!实际上,过去的正确答案可能是进程(特别是在UNIX系列的操作系统中)。因为早期的操作系统并不支持线程,为了创建线程,经常需要特殊的库函数支持。换言之,操作系统无法意识到线程的存在,而进程实际上成为运行的最小单位。即便在这种情况下,需要线程的程序员们也会利用特殊的库函数,以拆分进程运行时间的方式创建线程。但归根结底,这仅仅是应用程序级别创建的线程,与现在讨论的操作系统级别的线程存在巨大差异。
现代的Linux系列、Windows系列及各种规模不等的操作系统都在操作系统级别支持线程,因此,非显式创建线程的程序(如基于select的服务器端)可描述如下:“单一线程模型的应用程序”
反之,显式创建单独线程的程序可描述如下:“多线程模型的应用程序。”这就意味着main函数的运行同样基于线程完成,此时进程可以比喻为装有线程的篮子。实际的运行主体是线程。
调用该函数将创建线程,操作系统为了管理这些资源也将同时创建内核对象。最后返回用于区分内核对象的整数型“句柄”(Handle)。第1章已介绍过,句柄相当于Linux的文件描述符。
#include
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,SIZE_T dwStacksize,LPTHREAD_START_ROUTINE IpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId
);
//成功时返回线程句柄,失败时返回NULL。
IpThreadAttributes //线程安全相关信息,使用默认设置时传递NULL。
dwStackSize //要分配给线程的栈大小,传递0时生成默认大小的栈。
IpStartAddress //传递线程的main函数信息。
lpParameter //调用main函数时传递的参数信息。
dwCreationFlags //用于指定线程创建后的行为,传递0时,线程创建后立即进入可执行状态。
IpThreadld //用于保存线程ID的变量地址值。
上述定义看起来有些复杂,其实只需要考虑IpStartAddress和lpParameter这2个参数,剩下的只需传递0或NULL即可。
Windows 线程在首次调用的线程main 函数返回时销毁(销毁时间点和销毁方法与Linux不同)。还有其他方法可以终止线程,但最好的方法就是让线程main函数终止(返回)。
之前介绍过创建线程时使用的CreateThread函数,如果线程要调用C/C++标准函数,需要通过如
下方法创建线程。因为通过CreateThread函数调用创建出的线程在使用C/C++标准函数时并不稳定。
#include
uintptr_t _beginthreadex(void * security,unsigned stack _size,unsigned (* start_address)(void *),void * arglist,unsigned initflag,unsigned * thrdaddr);
//成功时返回线程句柄,失败时返回0。
上述函数与之前的CreateThread函数相比,参数个数及各参数的含义和顺序的相同,只是变
量名和参数类型有所不同。因此,用上述函数替换CreateThread函数时,只需适当更改数据类型。上述函数的返回值类型uintptr_t是64位unsigned整数型。但下述示例将通过声明CreateThread
函数的返回值类型HANDLE(这同样是整数型)保存返回的线程句柄。
#include
#include
#include
unsigned WINAPI ThreadFunc(void*arg); //这个WINAPI只是函数调用惯例声明,和链接时的操作有关
int main(int argc,char *argv[]){
HANDLE hThread;
unsigned threadID;
int param=5;
hThread=(HANDLE)_beginthreadex(NULL,0,ThreadFunc,(void*)¶m,0,&threadID);
if(hTread==0){
puts("_beginthreadex() error");
return -1;
}
sleep(3000);
puts("end of main");
return 0;
}
unsigned WINAPI ThreadFunc(void*arg){
int i;
int cnt=*((int*)arg);
for(i=0;i
与Linux相同,Windows同样在main函数返回后终止进程,也同时终止其中包含的所有线程。另外,如果对上述代码进行运行的话,最后输出的内容并非字符串"end of main",而是"running thread"。但这是在main函数返回后,完全销毁进程前输出的字符串。
线程也属于操作系统管理的资源,因此会伴随着内核对象的创建,并为了引用内核对象而返回句柄。可以利用句柄发送如下请求:“我会一直等到该句柄指向的线程终止。”可以通过句柄区分内核对象,通过内核对象可以区分线程。最终,线程句柄成为区分线程的工具。那线程ID又是什么呢?如上述示例所示,通过_beginthreadex函数的最后一个参数可以获取线程ID。各位或许对句柄和ID的并存感到困惑,其实它们有如下显著特点:“句柄的整数值在不同进程中可能出现重复,但线程在跨进程范围内不会出现重复。"线程ID用于区分操作系统创建的所有线程,但通常没有这种需求。
资源类型不同,内核对象也含有不同信息。其中,应用程序实现过程中需要特别关注的信息被赋予某种“状态”。例如,线程内核对象中需要重点关注线程是否已终止,所以终止状态又称“signaled状态”,未终止状态称为“non-signaled状态”。
我们通常比较关注进程的终止时间和线程的终止时间,所以自然会问:“该进程何时终止?”或“该线程何时终止?”操作系统将这些重要信息保存到内核对象,同时给出如下约定:“进程或线程终止时,我会把相应的内核对象改为signaled状态!"这也意味着,进程和线程的内核对象初始状态是non-signaled状态。那么,内核对象的signaled、non-signaled状态究竟如何表示呢?
非常简单!通过1个boolean变量表示。内核对象带有1个boolean变量,其初始值为FALSE,此时的状态就是non-signaled状态。如果发生约定的情况,把该变量改为TRUE,此时的状态就是signaled状态。内核对象类型不同,进入signaled状态的情况也有所区别(即对应事件也有区别)。
正常运行之前示例前需要考虑如下问题:“该内核对象当前是否为signaled状态?”
为回答类似问题,系统定义了WaitForSingleObject和WaitForMultipleObjects函数。
首先介绍WaitForSingleObject函数,该函数针对单个内核对象验证signaled状态。
#include
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
//成功时返回事件信息,失败时返回WAIT_FAILED。
hHandle //查看状态的内核对象句柄。
dwMilliseconds //以1/1000秒为单位指定超时,传递INFINITE时函数不会返回,直到内核对象变成
//signaled状态。
返回值 //进入signaled状态返回WAIT_OBJECT_0,超时返回WAIT_TIMEOUT。
该函数由于发生事件(变为signaled状态)返回时,有时会把相应内核对象再次改为non-signaled状态。这种可以再次进入non-signaled状态的内核对象称为“auto-reset模式”的内核对象,而不会自动跳转到non-signaled状态的内核对象称为“manual-reset模式”的内核对象。
即将介绍的函数与上述函数不同,可以验证多个内核对象状态。
#include
DWORD WaitForMultipleObeject(DWORD nCount,const HANDLE * lpHandles,BOOL bWaitALL,DWORD dwMilliseconds);
//成功时返回事件信息,失败时返回WAIT_FAILED。
nCount //需验证的内核对象数。
IpHandles //存有内核对象句柄的数组地址值。
bWaitAll //如果为TRUE,则所有内核对象全部变为signaled时返回;如果为FALSE,则只要有1
//个对象的状态变为signaled就会返回。
dwMilliseconds //以1/1000秒为单指定超时,传递INFINITE时函数不会返回,直到内核对象变为
//signaled状态。
下面利用WaitForSingleObject函数尝试解决示例的问题。
#include
#include
#include
unsigned WINAPI ThreadFunc(void *arg);
int main(int argc,char *argv[]){
HANDLE hTread;
DWORD wr;
unsigned threadID;
int param=5;
hTread=(HANDLE)_beginthreadex(NULL,0,ThreadFunc,(void*)¶m,0,&threadID);
if(hTread==0){
puts("_beginthreadex() error");
return -1;
}
if((wr=WaitForSingleObject(hThread,INFINITE))==WAIT_FAILED){
puts("thread wait error");
return -1;
}
printf("wait result: %s \n",(wr==WAIT_OBJECT_0)?"signaled":"time-out");
puts("end of main");
return 0;
}
unsigned WINAPI ThreadFunc(void *arg){
int i;
int cnt=*((int*)arg);
for(i=0;i
第18章在Linux平台下分析了临界区问题,本章最后的内容将留给Windows平台下的临界区
问题。
#include
#include
#include
#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
1ong long num=0;
int main(int argc, char *argv[]){
HANDLE tHandles[NUM_THREAD];
int i;
printf("sizeof long long: %d \n",sizeof(long,long));
for(i=0;i
即使多运行几次也无法得到正确结果,而且每次结果都不同。可以利用第20章的同步技术得到预想的结果。