2006年10月11日
第14章 在应用程序中使用虚拟内存
1Windows提供了3种进行内存管理的方法,它们是:
1)虚拟内存,最适合用来管理大型对象或结构数组。
2)内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。
3)内存堆栈,最适合用来管理大量的小对象。
使用虚拟内存:用于管理虚拟内存的函数可以用来直接保留一个地址空间区域,将物理存储器(来自页文件)提交给该区域,并且可以设置你自己的保护属性。
2.地址空间保留       通过调用VirtualAlloc函数,可以在进程的地址空间中保留一个区域:
PVOID VirtualAlloc(
PVOID pvAddress,
SIZE_T dwSize,
DWORD fdwAllocationType,// 能够告诉系统你想保留一个区域还是提交物理存储器
DWORD fdwProtect);//只以整个页面为单位同区域中不同页面可以使用不同保护属性
如果保留的区域预计在很长时间内不会被释放,那么可以在尽可能高的内存地址上保留该区域。这样,该区域就不会从进程地址空间的中间位置上进行保留。因为在这个位置上它可能导致区域分成碎片。如果想让系统在最高内存地址上保留一个区域,必须为pvAddress参数和fdwAllocationType参数传递NULL,还必须逐位使用ORMEM_TOP_DOWN标志和MEM_RESERVE标志连接起来。
3.在保留区域中的提交存储器      当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址。物理存储器总是按页面边界和页面大小的块来提交的。若要提交物理存储器,必须再次调用VirtualAlloc函数。不过这次为fdwAllocationType参数传递的是MEM_COMMIT标志,而不是MEM_RESERVE标志。
4.同时进行区域的保留和内存的提交有时你可能想要在保留区域的同时,将物理存储器提交给它。只需要一次调用VirtualAlloc函数就能进行这样的操作,如下所示:
PVOID pvMem=VirtualAlloc(NULL,99*1024,MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
这个函数调用请求保留一个99KB的区域,并且将99KB的物理存储器提交给它。当系统处理这个函数调用时,它首先要搜索你的进程的地址空间,找出未保留的地址空间中一个地址连续的区域,它必须足够大,能够存放100KB(在4KB页面的计算机上)或104KB(在8KB页面的计算机上)。
5.何时提交物理存储器   假设想实现一个电子表格应用程序,对于每一个单元格,都需要一个CELLDATA结构来描述单元格的内容。若要处理这种二维单元格矩阵,最容易的方法是声明一个很大的CELLDATA数组。但是如果直接用页文件来分配物理存储器,那么这是个不小的数目了,尤其是考虑到大多数用户只是将信息放入少数的单元格中,而大部分单元格却空闲不用,因此显得有些浪费。内存的利用率非常低。传统上,可用链表等实现。但是这种方法使得你很难获得单元格的内容。如果想知道第5行第10列的单元格的内容,必须遍历链接表,才能找到需要的单元格,因此使用链接表方法比明确声明的矩阵方法速度要慢。
虚拟内存为我们提供了一种兼顾预先声明二维矩阵和实现链表的两全其美的方法。运用虚拟内存,既可以使用已声明的矩阵技术进行快速而方便的访问,又可以利用链接表技术大大节省内存的使用量。
如果想利用虚拟内存技术的优点,你的程序必须按照下列步骤来编写:
1)保留一个足够大的地址空间区域,用来存放CELLDATA结构的整个数组。
2)当用户将数据输入一个单元格时,找出CELLDATA结构应该进入的保留区域中的内存地址。
3)CELLDATA结构来说,确定是否提交物理存储器给第二步中找到的内存地址。
4)设置新的CELLDATA结构的成员。
虚拟内存技术存在的一个问题是,必须确定物理存储器在何时提交。如果用户将数据输入一个单元格,然后只是编辑或修改该数据,那么就没有必要提交物理存储器,因为该单元格的CELLDATA结构的内存在数据初次输入时就已经提交了。另外,系统总是按页面的分配粒度来提交物理存储器的。因此,当试图为单个CELLDATA结构提交物理存储器时(像上面的第二步那样),系统实际上提交的是内存的一个完整的页面。确定是否要提交物理存储器给的方法:
1始终设法进行物理存储器的提交。每次调用VirtualAlloc函数的时候,始终让你的程序设法进行内存的提交。系统会查看内存是否已经被提交,如果已经提交,那么就不要提交更多的物理存储器。
2)(使用VirtualQuery)确定存储器是否已经提交给包含CELLDATA结构的地址空间。
3)保留一个关于哪些页面已经提交和哪些页面尚未提交的记录。
4)使用结构化异常处理(SEH)方法SEH是一个操作系统特性,它使系统能够在发生某种情况时将此情况通知你的应用程序。实际上可以创建一个带有异常处理程序的应用程序,然后,每当试图访问未提交的内存时,系统就将这个问题通知应用程序。然后你的应用程序便进行内存的提交,并告诉系统重新运行导致异常条件的指令。这时对内存的访问就能成功地进行了,程序将继续运行,仿佛从未发生过问题一样。
6.回收虚拟内存和释放地址空间区域若要回收映射到一个区域的物理存储器,或者释放这个地址空间区域,可调用VirtualFree函数:
BOOL VirtualFree(
LPVOID pvAddress,
SIZE_T dwSize,
DWORD fdwFreeType);
7.何时回收物理存储器   再以电子表格为例。如果你的应用程序是在x86计算机上运行,每个内存页面是4KB,它可以存放32个(4096/128CELLDATA结构。如果用户删除了单元格CellData[0][1]的内容,那么只要单元格CellData[0][0]CellData[0][31]也不被使用,就可以回收它的内存页面。那么怎么能够知道这个情况呢?可以用下面3种方法来解决:
1)最容易方法是设计一CELLDATA结构,它的大小只有一个页面。这时,由于始终都是每个页面使用一结构,因此当不再需要该结构中数据时,就可以回收该页面物理存储器。
2)保留一个正在使用的结构的记录。为了节省内存,可以使用一个位图来跟踪。这样,如果有一个100个结构的数组,你也可以维护一个100位的数组。开始时,所有的位均设置为0,表示这些结构都没有使用。当使用这些结构时,可以将对应的位设置为1。然后,每当不需要某个结构,并将它的位重新改为0时,你可以检查属于同一个内存页面的相邻结构的位。如果没有相邻的结构正在使用,就可以回收该页面。
3)最后一个方法是实现一个无用单元收集函数。当你的应用程序运行时,必须定期调用无用单元收集函数。该函数应该遍历所有潜在的数据结构。对于每个数据结构,该函数首先要确定是否已经为该结构提交内存。如果已经提交,该函数将检查fInUse成员,以确定它是否是0。如果该值是0,则表示该结构没有被使用。如果该值是TRUE,则表示该结构正在使用。当无用单元函数检查了属于既定页面的所有结构后,如果所有结构都没有被使用,它将调用VirtualFree函数,回收该内存。
当一个结构不再被视为“在用”(InUse)后,就可以立即调用无用单元收集函数,不过这项操作需要的时间比你想像的要长,因为该函数要循环通过所有可能的结构。实现该函数的一个出色方法是让它作为低优先级线程的一部分来运行。
8.改变保护属性       虽然实践中很少这样做,但是可以改变已经提交的物理存储器的一个或多个页面的保护属性。若要改变内存页面的保护属性,可以调用VirtualProtect函数:
BOOL VirtualProtect(
PVOID pvAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD pfloldProtect);
9.       地址窗口扩展
随着时间的推移,应用程序需要的内存越来越多。对于服务器应用程序来说,更是如此。由于越来越多的客户机对服务器提出访问请求,服务器的运行性能就会降低。为了提高运行性能,服务器应用程序必须在RAM中保存更多的数据,并且缩小磁盘的页面。对于所有这些应用程序来说,32位地址空间是不够使用的。为了满足这些应用程序的需要,Windows2000提供了一个新特性。称为地址窗口扩展(AWE)。Microsoft创建AWE是出于下面两个目的:
1)允许应用程序对从来不在操作系统与磁盘之间交换的RAM进行分配。
2)允许应用程序访问的RAM大于进程的地址空间。

64Windows2000全面支持AWE。对使用AWE32位应用程序进行移植是非常容易和简单的。不过对于64位应用程序来说,AWE的用处比较小,因为进程的地址空间太大了。

第14章虚拟内存
1.系统信息许多操作系统的值是根据主机而定的,比如页面的大小,分配粒度的大小等。这些值决不应该用硬编码的形式放入你的源代码。相反,你始终都应该在进程初始化的时候检索这些值,并在你的源代码中使用检索到的值。GetSystemInfo函数将用于检索与主机相关的值。由于有了GetSystemInfo函数,因此应用程序能够在运行的时候查询这些值。对于任何既定的系统来说,这些值总是相同的,因此决不需要为任何既定的进程多次调用该函数。
2.虚拟内存的状态   Windows函数GlobalMemoryStatus可用于检索关于当前内存状态的动态信息。当调用GlobalMemoryStatus时,必须传递一个MEMORYSTATUS结构的地址。下面显示了MOMORYSTATUS的数据结构。
Typedef struct _MEMORYSTATUS{
DWORD dwLength;
DWORD dwMemoryLoad;
SIZE_T dwTotalphys;
SIZE_T dwAvailphys;
SIZE_T dwTotalPageFile;
SIZE_T dwAvailPageFile;
SIZE_T dwTotalVirtual;
SIZE_T dwAvailVirtual;
} MEMORYSTATUS,*LPMEMORYSTATUS;
如果希望应用程序在内存大于4GB的计算机上运行,或者合计交换文件的大小大于4GB,那么可以使用新的GlobalMemoryStatusEx函数。必须给该函数传递新的MEMORYSTATUSEX结构的地址。这个结构与原先的MEMORYSTATUS结构基本相同,差别在于新结构的所有成员的大小都是64位宽DWORDLONG,因此它的值可以大于4GB。
3.确定地址空间的状态   Windows提供了一个函数,可以用来查询地址空间中内存地址的某些信息(如大小,存储器类型和保护属性等)。这个函数称为VirtualQuery
DWORD VirtualQuery(
  LPCVOID pvAddress,
  PMEMORY_BASIC_INFORMATION pmbi,
  DWORD dwLength);
Windows还提供了另一个函数,它使一个进程能够查询另一个进程的内存信息:
DWORD VirtualQueryEX(
  HANDLE hProcess,
  LPCVOID pvAddress,
  PMEMORY_BASIC_INFORMATION pmbi,
  DWORD dwLength);

这两个函数基本相同,差别在于使用VirtualQueryEx时,可以传递你想要查询的地址空间信息的进程的句柄。调试程序和其他实用程序使用这个函数最多,几乎所有的应用程序都只需要调用VirtualQuery函数。

2006年10月07日
第13章 Windows内存结构
1.进程的虚拟地址空间   每个进程都被赋予它自己的虚拟地址空间。这是个虚拟地址空间,不是物理地址空间。该地址空间只是内存地址的一个范围。在你能够成功地访问数据而不会出现违规访问之前,必须赋予物理存储器,或将物理存储器映射到各个部分的地址空间。
2Win2KWin98内存结构区别 注意在Win2000中,属于操作系统本身的内存也是隐藏的,正在运行的线程无法访问。这意味着线程常常不能访问操作系统的数据。Windows98中,属于操作系统的内存是不隐藏的,正在运行的线程可以访问。因此,正在运行的线程常常可以访问操作系统的数据,也可以破坏操作系统(从而有可能导致操作系统崩溃)。Windows98中,一个进程的线程不可能访问属于另一个进程的内存。
3.虚拟地址空间如何分区      每个进程的虚拟地址空间都要划分成各个分区。地址空间的分区是根据操作系统的基本实现方法来进行的。不同的Windows内核,其分区也略有不同。
31 NULL指针分配的分区
保护这个分区是极其有用的,它可以帮助你发现异常。C/C++程序中常常不进行严格的错误检查。例如,下面这个代码就没有进行任何错误检查:
Int* pnSomeInteger = (int*) malloc(sizeof(int)); *pnSomeInteger =5;
如果malloc不能找到足够的内存来满足需要,它就返回NULL。但是,该代码并不检查这种可能性,它认为地址的分配已经取得成功,并且开始访问0×00000000地址的内存。由于这个分区的地址空间是禁止进入的,因此就会发生内存访问违规现象,同时该进程将终止运行。这个特性有助于编程员发现应用程序中的错误。
32 用户方式分区    这个分区是进程的私有(非共享)地址空间所在的地方。由于每个进程可以得到它自己的私有的、非共享分区,以便存放它的数据,因此,应用程序不太可能被其他应用程序所破坏,这使得整个系统更加健壮。
在Windows2000中,所有的.exe和DLL模块均加载这个分区。每个进程可以将这些DLL加载到该分区的不同地址中(不过这种可能性很小)。系统还可以在这个分区中映射该进程可以访问的所有内存映射文件。
可以使用的地址空间还不到我的进程的全部地址空间的一半。难道内核方式分区真的需要上面的一半地址空间吗?实际回答是肯定的。系统需要这个地址空间,供内核代码、设备驱动程序代码、设备I/O高速缓存、非页面内存池的分配和进程页面表等使用。实际上Microsoft将内核压缩到这个2GB空间之中。
33 64KB禁止进入的分区      访问该分区中的内存的任何企图均将导致访问违规。这样使得用户方式代码不可能误操作改写内核方式代码访问的内存。
34 内核方式分区    这个分区是存放操作系统代码的地方。用于线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序的代码全部在这个分区加载。驻留在这个分区中的一切均可被所有进程共享。在Windows2000中,这些组件是完全受到保护的。
4.保留地址空间中的区域      若要使用进程地址空间,须通过VirtualAlloc函数保留(reserving)。保留时系统要确保该区域从一个分配粒度的边界开始。不同CPU平台分配粒度不相同。但是,至今所有的CPU平台(x86、32位Alpha、64位Alpha和IA-64)都使用64KB这个相同的分配粒度。另,系统要确保该区域是系统页面大小的倍数。页面是系统在管理内存时使用的一个内存单位。与分配粒度一样,不同的CPU,其页面大小也是不同的。x86使用的页面大小是4KB,而Alpha使用的页面大小则是8KB。当你的程序算法不再需要访问已经保留的地址空间区域时,应该释放。这个过程称为释放地址空间的区域,它是通过调用VirtualFree函数来完成的。
5.提交地址空间区域中的物理存储器若要使用已保留的地址空间区域,必须分配物理存储器,然后将物理存储器映射到已保留的地址空间区域。这个过程称为提交物理存储器。物理存储器总是以页面的形式来提交的。提交也要调用VirtualAlloc函数。当不再需要已提交的物理存储器时,该物理存储器应该被释放。这个过程称为回收物理存储器,它是通过VirtualFree函数来完成的。
6.物理存储器与页文件   在较老的操作系统中,物理存储器被视为计算机拥有的RAM的容量。今天的操作系统能使得磁盘空间看上去就像内存一样。磁盘上的文件通常称为页文件,它包含了可供所有进程使用的虚拟内存。如果多个页文件存在于不同的物理硬盘驱动器上,系统的运行将能得快得多,因为它能够将数据同时写入多个驱动器。
操作系统与CPU相协调,将RAM的各个部分保存到页文件中,当运行的应用程序需要时,再将页文件的各个部分重新加载到RAM。鼓励用户使用页文件,这样他们就能够运行更多的应用程序,并且这些应用程序能够对更大的数据集进行操作。
在第二种情况中,线程试图访问的数据不在RAM中,而是存放在页文件中的某个地方。这时,试图访问就称为页面失效,CPU将把试图进行的访问通知操作系统。这时操作系统就寻找RAM中的一个内存空页。所以必须遵循一条基本原则,那就是要让你的计算机运行得更块,增加更多的RAM实际上,在大多数情况下,增加RAM比提高CPU的速度所产生的效果更好。
7可能你会认为,运行一个程序时,系统必须为进程的代码和数据保留地址空间的一些区域,将物理存储器提交给这些区域,然后将代码和数据从硬盘上的程序文件拷贝到页文件中已提交的物理存储器中。实际上系统并不如此。相反,当启动程序时,系统并不是从页文件中分配地址空间,而是将.exe文件的实际内容即映像用作程序的保留地址空间区域。这使应用程序的加载非常迅速,并使页文件能够保持得非常小。这称为内存映射文件。
8.保护属性已经分配的物理存储器的各个页面可以被赋予不同的保护属性。PAGE_NOACCESS    PAGE_READONLY  PAGE_READWRITE   PAGE_EXECUTE   PAGE_EXECUTE_READ      PAGE_EXECUTE_READWRITE   PAGE_WRITECOPY   PAGE_EXECUTE_WRITECOPY    
9Copy-On-Write访问     Windows支持一种机制,使得两个或多个进程能够共享单个内存块。因此,如果10个Notepad实例正在运行,那么所有实例可以共享应用程序的代码和数据页面。让所有实例共享同样的内存页面将能够大大提高系统的性能,但是这要求所有实例都将该内存视为只读或只执行的内存。如果一个实例中的线程将数据写入内存修改它,那么其他实例看到的这个内存也将被修改,从而造成混乱。为了防止出现这种混乱,操作系统给共享内存块赋予了Copy-On-Write保护属性。
当一个.exe或DLL模块被映射到一个内存地址时,系统将计算有多少页面是可以写入的。然后,从页文件中分配内存,以适应这些可写入的页面的需要。当一个进程中的线程试图将数据写入一个共享内存块时,系统就会进行干预,并执行下列操作步骤:
1)系统查找RAM中的一个空闲内存页面。
2)系统将试图被修改的页面内容拷贝到第一步中找到的页面。
3)然后系统更新进程的页面表,使得被访问的虚拟地址被转换成新的RAM页面。
当系统执行了这3个操作步骤之后,该进程就可以访问它自己的内存页面的私有实例。
10.数据对齐的重要性     数据对齐并不是操作系统的内存结构的一部分,而是CPU结构的一部分。当CPU访问正确对齐的数据时,它的运行效率最高。当CPU试图读取的数据值没有正确对齐时,CPU可以执行两种操作之一。即它可以产生一个异常条件,也可以执行多次对齐的内存访问,以便读取完整的未对齐数据值。显然,如果CPU执行多次内存访问,应用程序的运行速度就会放慢。
1x86CPU进行数据对齐 X86CPU的EFLAGS寄存器中包含一个特殊的位标志,称为AC(对齐检查的英文缩写)标志。当CPU首次加电时,该标志被设置为0。当该标志是0时,CPU能够自动执行它应该执行的操作,以便成功地访问未对齐的数据值。然而,如果该标志被设置为1,每当系统试图访问未对齐的数据时,CPU就会发出一个INT17H中断。
2AlphaCPU的情况   AlphaCPU不能自动处理对未对齐数据的访问。当未对齐的数据访问发生时,CPU就会将这一情况通知操作系统。这时,Windows2000将会确定它是应该引发一个数据未对齐异常条件还执行一些辅助指令,对问题默默地加以纠正,并让你的代码继续运行。按默认设置,当在Alpha计算机上安装Windows2000时,操作系统会对未对齐数据的访问默默地进行纠正。然而,可以手动改变这个行为特性。
2006年09月30日

第12章 纤程
1. 纤程产生 UNIX服务器应用程序属于单线程应用程序(由Windows定义),但是能为多个客户程序提供服务。换句话说,UNIX应用程序的开发人员已经创建了他们自己的线程结构库,他们能够使用这种线程结构库来仿真线程。该线程包能够创建多个堆栈,保存某些CPU寄存器,并且在它们之间进行切换,以便为客户机请求提供服务。MS添加了一种纤程,以便能够非常容易地将现有的UNIX服务器应用程序移植到Windows中。
2. 纤程  实现线程的是Windows内核。操作系统清楚地知道线程的情况,并且根据MS定义的算法对线程进行调度。纤程是以用户方式代码来实现的,内核并不知道纤程,并且它们是根据用户定义的算法来调度的。就内核而言,纤程采用非抢占式调度方式。另外,单线程可以包含一个或多个纤程。就内核而言,线程是抢占调度的,是正在执行的代码。然而,线程每次执行一个纤程的代码你决定究竟执行哪个纤程
3.函数     
1当使用纤程时,你必须执行的第一步操作是将现有的线程转换成一个纤程。可以通过调用PVOID ConvertThreadToFiber(PVOID pvParam)函数来执行这项操。当对纤程的执行环境进行分配和初始化后,就可以将执行环境的地址与线程关联起来。该线程被转换成一个纤程,而纤程则在该线程上运行。ConvertThreadToFiber函数实际上返回纤程的执行环境的内存地址。虽然必须在晚些时候使用该地址,但是决不应该自己对该执行环境数据进行读写操作,因为必要时纤程函数会为你对该结构的内容进行操作。现在,如果你的纤程(线程)返回或调用ExitThread函数,那么纤程和线程都会终止运行。
2) 若要创建另一个纤程,该线程(当前正在运行纤程的线程)可以调用CreateFiber函数。
3)在单个线程上,每次只能运行一个纤程。若要使新纤程能够运行,可以调用SwitchToFiber函数。该函数是纤程获得CPU时间的唯一途径。
4) 若要撤消纤程,可以调用VOID DeleteFiber(PVOID pvFiberExecutionContext)函数,如果传递了当前与线程相关联的纤程地址,那么该函数就在内部调用ExitThread函数,该线程及其创建的所有纤程全部被撤消。

5) 一个线程每次可以执行一个纤程,操作系统始终都知道当前哪个纤程与该线程相关联。如果想要获得当前运行的纤程的执行环境的地址,可以调用PVOID GetCurrentFiber()函数。

2006年09月28日

12 纤程

1. 纤程产生  UNIX服务器应用程序属于单线程应用程序(由Windows定义),但是能为多个客户程序提供服务。换句话说,UNIX应用程序的开发人员已经创建了他们自己的线程结构库,他们能够使用这种线程结构库来仿真线程。该线程包能够创建多个堆栈,保存某些CPU寄存器,并且在它们之间进行切换,以便为客户机请求提供服务。MS添加了一种纤程,以便能够非常容易地将现有的UNIX服务器应用程序移植到Windows中。

2. 纤程  实现线程的是Windows内核。操作系统清楚地知道线程的情况,并且根据MS定义的算法对线程进行调度。纤程是以用户方式代码来实现的,内核并不知道纤程,并且它们是根据用户定义的算法来调度的。就内核而言,纤程采用非抢占式调度方式。另外,单线程可以包含一个或多个纤程。就内核而言,线程是抢占调度的,是正在执行的代码。然而,线程每次执行一个纤程的代码你决定究竟执行哪个纤程

3.函数     

1当使用纤程时,你必须执行的第一步操作是将现有的线程转换成一个纤程。可以通过调用PVOID ConvertThreadToFiber(PVOID pvParam)函数来执行这项操。当对纤程的执行环境进行分配和初始化后,就可以将执行环境的地址与线程关联起来。该线程被转换成一个纤程,而纤程则在该线程上运行。ConvertThreadToFiber函数实际上返回纤程的执行环境的内存地址。虽然必须在晚些时候使用该地址,但是决不应该自己对该执行环境数据进行读写操作,因为必要时纤程函数会为你对该结构的内容进行操作。现在,如果你的纤程(线程)返回或调用ExitThread函数,那么纤程和线程都会终止运行。

2) 若要创建另一个纤程,该线程(当前正在运行纤程的线程)可以调用CreateFiber函数。

3) 在单个线程上,每次只能运行一个纤程。若要使新纤程能够运行,可以调用SwitchToFiber函数。该函数是纤程获得CPU时间的唯一途径。

4) 若要撤消纤程,可以调用VOID DeleteFiber(PVOID pvFiberExecutionContext)函数,如果传递了当前与线程相关联的纤程地址,那么该函数就在内部调用ExitThread函数,该线程及其创建的所有纤程全部被撤消。

5) 一个线程每次可以执行一个纤程,操作系统始终都知道当前哪个纤程与该线程相关联。如果想要获得当前运行的纤程的执行环境的地址,可以调用PVOID GetCurrentFiber()函数。

2006年09月24日

9 线程与内核对象的同步

可以用于同步的内核对象可以处于通知状态和未通知状态。线程可以等待这些对象。如果被等待对象处于已通知状态,则线程变为可调用;如果处于未通知状态,则线程阻塞。

1. 等待函数  等待函数是WaitForSingleObjectWaitForMultipleObjects前者用来等待单一的内核对象。当被等待对象为未通知状态时,线程阻塞,直到等待成功或指定的时间结束;后者用来等待多个内核对象。可以设置等待所有对象都变为已通知时才唤醒线程;或者其中任何一个变为已通知就唤醒线程。WaitForMultipleObjects是以原子操作运行的等待成功后可能会改变对象的属性,这称为成功等待的副作用。无论对等待单个对象还是多个对象,副作用都是在等待成功时一次性执行的。

2. 进程内核对象  当进程正在运行时,进程内核对象处于未通知状态。当进程停止运行时,就处于已通知状态。可以通过等待进程来检查进程是否仍然运行。无成功等待的副作用。

3. 线程内核对象  当线程正在运行时,线程内核对象处于未通知状态。当线程停止运行时,就处于已通知状态。可以通过等待线程来检查线程是否仍然运行。无成功等待的副作用。

4. 事件内核对象  包括人工重置的事件和自动重置的事件

1)  当人工重置事件得到通知时,等待该事件的所有线程成为可调度线程;它没有成功等待副作用。

2)  当自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。其成功等待的副作用是该对象自动重置为未通知状态。

       事件内核对象通过CreateEvent创建,初始可以是通知或未通知状态。SetEvent将事件改为已通知状态,ResetEvent将事件设为未通知状态。当一个线程执行初始化操作,然后通知另一个线程执行剩余的操作时,经常使用人工重置事件对象。另如果一个写线程,多个读线程,可以让写线程完成写操作时通过人工事件通知读线程读取数据。而自动事件对象则可以用于保护资源在同一时间只有一个线程可以访问,因为它保证只有一个线程被激活。

5. 等待定时器内核对象  等待定时器是在某个时间或按规定的时间间隔发出自己的信号通知的内核对象。包括人工重置的定时器和自动重置的定时器。初始必须是未通知状态。

       1当发出人工重置的定时器信号时,等待该定时器的所有线程变为可调度;无成功等待副作用。

       2当发出自动重置的定时器信号时,只有一个等待线程变为可调度线程。成功等待副作用是重置对象。通过CreatWaitableTimer创建,CancelWaitableTimer撤销一个定时器,SetWaitableTimer告诉定时器何时让其变为已通知状态。

6 信号量内核对象  信号量用来对资源进行计数。它包含两个32位值,一个表示能够使用的最大资源数量,一个表示当前可用的资源数量

       信号量的使用规则如下:

1. 如果当前资源数量大于0,发出信号量信号

2. 如果当前资源数量是0,不发出信号量信号

3. 不允许当前资源数量为负值

4. 当前资源数量不能大于最大信号数量 

通过CreateSemaphore创建。ReleaseSemaphore来释放,从而使当前资源数量增加。

当调用等待函数时,它会检查信号量的当前资源数量。如果它的值大于0,那么计数器减1,调用线程处于可调度状态。如果当前资源是0,则调用函数的线程进入等待状态。当另一线程对信号量的当前资源通过ReleaseSemaphore进行递增时,系统会记住该等待线程,并将其变为可调度。当有多个资源共同访问时,经常使用信号量内核对象。成功等待副作用是当前资源数量减1。(计数器即为当前资源数量,与内核对象本身的引用计数器两码事)

7.   互斥对象  互斥器保证线程拥有对单个资源的互斥访问权。互斥对象类似于关键代码区,但它是一个内核对象。互斥器不同于其他内核对象,它有一个线程所有权的概念。它如果被某个线程等待成功,就属于该线程。

       互斥器的使用规则如下:

1. 如果线程ID0(无效ID),互斥对象不被任何线程拥有,并且发出该互斥对象的通知信号。

2. 如果ID是非0数字,那么一个线程可以拥有互斥对象,并且不发出该互斥对象的通知信号。

3. 互斥器有一个递归计数器。如果线程已经拥有了互斥器,而它再次等待该互斥器,则马上成功返回;而且递归计数器加1

通过CreateMutex创建。ReleaseMutex用来释放互斥器,如果线程拥有互斥器,则首先把递归计数器减1,如果减到0,则线程释放互斥器,或者说互斥器的所属线程为空。即如果多次成功等待一个互斥对象,则应以同样次数调用ReleaseMutex函数。此后其他线程就可以等待得到该互斥器了。但是如果一个线程ReleaseMutex了一个本来不归他所有的互斥器,则不会有任何效果。

       互斥器常用于保护由多个线程访问的内存块。互斥器保证了访问内存块的任何线程拥有对该内存块的独占访问。其成功等待副作用是将所有权赋予线程,并将递归计数器加1

8 等待定时器与用户定时器  分别为内核对象与用户对象,区别为:

1)用户定时器能生成WM_TIMER消息,这些消息将返回给调用SetTimer的线程和创建窗口(用于基于窗口的定时器)的线程。因此,当用户定时器报时的时候,只有一个线程得到通知。另一方面,多个线程可以在等待定时器上等待,如果定时器是个人工重置的定时器,则可以调度若干个线程。

2WM_TIMER消息始终属于最低优先级消息,当线程的队列中没有其他消息时,才检索该消息。等待定时器的处理方法与其他内核对象没有什么差别。因此等待定时器在到达规定时间后更有可能得到通知。

2006年09月22日

8 用户方式中线程的同步

线程同步可以采用多种方式。可以在用户方式下实现,也可以在内核方式下实现。前者的优势在于速度快,因为不用在用户方式和内核方式之间切换,但只能用于同一个进程内的线程之间的同步;后者是使用内核对象的方式,速度虽慢,但可以用于不同进程之间的线程同步。而且后者相对前者方法丰富许多,功能也强大许多

1.线程通信  系统中的所有线程都必须拥有对各种资源的访问权,这些资源包括内存堆栈、串口、文件、窗口和其他许多资源。线程需要在以下两种情况下进行通信:

       1)当有多个线程访问共享资源而不使资源被破坏时

       2)当一个现成需要将某个任务已经完成的情况通知另一个或多个线程时。

2.原子访问与互锁函数   原子访问是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。自加与自减非原子操作互锁函数使用一种手段来保证值的增减以原子操作方式实现InterlockedExchangeAddInterlockedExchange等。循环锁是利用互锁函数InterlockedExchange来实现的一种线程同步方式。

BOOL g_bResourceInUse = FALSE;

void func(){

//wait to access the resource

while (InterlockedExchange(&g_bResourceInUse, TRUE) == TRUE)

{  Sleep(0);  }

    //access the resource

    … // InterlockedExchange返回exchange前的变量值

    //no longer need to access the resource

    InterlockedExchange(&g_bResourceInUse, FALSE);

} 

3CPU的高速缓存行       当一个cpu从内存读取一个字节时,它要取出足够的字节来填入高速缓存行。高速缓存行由3264个字节组成,其作用是提高cpu运行的性能,因为通常情况下,应用程序只能对一组相邻的字节进行处理。如果这些字节在高速缓存行中,那么cpu就不必访问内存总线,而访问内存总线需要多得多的时间。但是,为了防止数据不同步,当一个cpu的高速缓存行中字节被更改时,其他cpu会被通知,且他们的高速缓存行变为无效

4.关键代码段  是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。即让若干行能够以原子方式来使用资源。实施的方式是在程序中加入“进入”或“离开”的操作。如果一个线程进入了CS,另外一个线程绝对不能进入该CS为实现这种功能MS提供了五个函数:

InitializeCriticalSection:创建CRITICAL_SECTION类型的变量。它不是内核对象,所以不是返回句柄。它存在于进程的内存空间中。关于使用CS的线程handle以及使用计数都保存在该结构中。DeleteCriticalSection负责删除CS

    EnterCriticalSection:在进入CS前必须调用该函数。这样可以保证之后的代码在同一时间内只有一个线程可以进入。它会查看CRITICAL_SECTION结构,从而保证这一点。具体方法是:

1.如果没有别的线程进入,则进入。并设置critical section为自己线程所访问。

2.如果线程自己正在访问该critical section,则只是将计数加1,然后进入。

3.如果别的线程访问critical section,则等待。系统会在线程释放CS资源后更新CRITICAL_SECTION,从而唤醒等待线程。

LeaveCriticalSection:查看结构中的成员变量。该函数每次计数都要递减1,指明调用线程多少次被赋予对共享资源的访问权。如果计数大于0,则该函数不做其它操作,只是返回;如果等于0,说明该线程释放了资源,所以该函数查看调用是否有别的线程在等待。如果至少有一个在等待,则更新成员变量,唤醒等待线程。

TryEnterCriticalSection用来判断线程能否进入critical section,它马上返回结果。

对于同一个CRITICAL SECTION可以多次调用Enter,但必须调用同样次数的LeaveEnter只可能使其它请求使用该对象的线程阻塞,如果本身正在使用该关键区,则只是让计数加1,不会自己阻塞自己。

5.关键代码段与循环锁  当线程试图进入已被另外线程拥有的CS时,调用线程被立即置于等待状态,这意味着必须从用户态转为内核态(1000cpu周期),在拥有多cpu情况时,因为线程可能会很快leavecriticalsection,因而这种转换相对来说需要付出更大的代价。因此MS将解决方案设计为在进入等待状态前,利用循环锁以设法多次取得该资源。为此提供了InitialzeCriticalSectionAndSpinCountSetCriticalSectionSpinCount函数。

6.使用技巧  所有线程的需要使用共享资源的代码都必须封装在EnterLeave间。对于含多个共享资源的情况需要使用多个CS变量,此时各线程中进入各CS变量的顺序应一致以防死锁。另外,不要长时间运行关键代码段,其他线程的等待会降低应用程序的运行性能。

2006年09月20日

我现在是自己做,但我此前有多年在从事软件开发工作,当回过头来想一想自己,觉得特别想对那些初学JAVA/DOT。NET技术的朋友说点心里话,希望你们能从我们的体会中,多少受点启发(也许我说的不好,你不赞同但看在我真心的份上别扔砖头啊).

一。 在中国你千万不要因为学习技术就可以换来稳定的生活和高的薪水待遇,你千万更不要认为哪些从事 市场开发,跑腿的人,没有前途。

不知道你是不是知道,咱们中国有相当大的一部分软件公司,他们的软件开发团队都小的可怜,甚至只有1-3个人,连一个项目小组都算不上,而这样的团队却要 承担一个软件公司所有的软件开发任务,在软件上线和开发的关键阶段需要团队的成员没日没夜的加班,还需要为测试出的BUG和不能按时提交的软件模块功能而 心怀忐忑,有的时候如果你不幸加入现场开发的团队你则需要背井离乡告别你的女友,进行封闭开发,你平时除了编码之外就是吃饭和睡觉(有钱的公司甚至请个保 姆为你做饭,以让你节省出更多的时间来投入到工作中,让你一直在那种累了就休息,不累就立即工作的状态)

更可怕的是,会让你接触的人际关系非常单一,除了有限的技术人员之外你几乎见不到做其他行业工作和职位的人,你的朋友圈子小且单一,甚至破坏你原有的爱情(想象一下,你在外地做现场开发2个月以上,却从没跟女友见过一面的话,你的女友是不是会对你呲牙裂嘴)。

也许你拿到了所谓的白领的工资,但你却从此失去享受生活的自由,如果你想做技术人员尤其是开发人员,我想你很快就会理解,你多么想在一个地方长期待一段时间,认识一些朋友,多一些生活时间的愿望。

比之于我们的生活和人际关系及工作,那些从事售前和市场开发的朋友,却有比我们多的多的工作之外的时间,甚至他们工作的时间有的时候是和生活的时间是可以 兼顾的,他们可以通过市场开发,认识各个行业的人士,可以认识各种各样的朋友,他们比我们坦率说更有发财和发展的机会,只要他们跟我们一样勤奋。(有一种 勤奋的普通人,如果给他换个地方,他马上会成为一个勤奋且出众的人。)

二。在学习技术的时候千万不要认为如果做到技术最强,就可以成为100%受尊重的人。

有一次一个人在面试项目经理的时候说了这么一段话:我只用最听话的人,按照我的要求做只要是听话就要,如果不听话不管他技术再好也不要。随后这个人得到了试用机会,如果没意外的话,他一定会是下一个项目经理的继任者。

朋友们你知道吗?不管你技术有多强,你也不可能自由的腾出时间象别人那样研究一下LINUX源码,甚至写一个LINUX样的杰作来表现你的才能。你需要做 的就是按照要求写代码,写代码的含义就是都规定好,你按照规定写,你很快就会发现你昨天写的代码,跟今天写的代码有很多类似,等你写过一段时间的代码,你 将领略:复制,拷贝,粘贴那样的技术对你来说是何等重要。(如果你没有做过1年以上的真正意义上的开发不要反驳我)。

如果你幸运的能够听到市场人员的谈话,或是领导们的谈话,你会隐约觉得他们都在把技术人员当作编码的机器来看,你的价值并没有你想象的那么重要。而在你所 在的团队内部,你可能正在为一个技术问题的讨论再跟同事搞内耗,因为他不服你,你也不服他,你们都认为自己的对,其实你们两个都对,而争论的目的就是为了 在关键场合证明一下自己比对方技术好,比对方强。(在一个项目开发中,没有人愿意长期听别人的,总想换个位置领导别人。)

三。你更不要认为,如果我技术够好,我就自己创业,自己有创业的资本,因为自己是搞技术的。

如果你那样认为,真的是大错特错了,你可以做个调查在非技术人群中,没有几个人知道C#与JAVA的,更谈不上来欣赏你的技术是好还是不好。一句话,技术 仅仅是一个工具,善于运用这个工具为别人干活的人,却往往不太擅长用这个工具来为自己创业,因为这是两个概念,训练的技能也是完全不同的。

创业最开始的时候,你的人际关系,你处理人际关系的能力,你对社会潜规则的认识,还有你明白不明白别人的心,你会不会说让人喜欢的话,还有你对自己所提供 的服务的策划和推销等等,也许有一万,一百万个值得我们重视的问题,但你会发现技术却很少有可能包含在这一万或一百万之内,如果你创业到了一个快成功的阶 段,你会这样告诉自己:我干吗要亲自做技术,我聘一个人不就行了,这时候你才真正会理解技术的作用,和你以前做技术人员的作用。

[小结]

基于上面的讨论,我奉劝那些学习技术的朋友,千万不要拿科举考试样的心态去学习技术,对技术的学习几近的痴迷,想掌握所有所有的技术,以让自己成为技术领域的权威和专家,以在必要的时候或是心里不畅快的时候到网上对着菜鸟说自己是前辈。

技术仅仅是一个工具,是你在人生一个阶段生存的工具,你可以一辈子喜欢他,但最好不要一辈子靠它生存。

掌握技术的唯一目的就是拿它找工作(如果你不想把技术当作你第二生命的话),就是干活。所以你在学习的时候千万不要去做那些所谓的技术习题或是研究那些帽泡算法,最大数算法了,什么叫干活?

就是做一个东西让别人用,别人用了,可以提高他们的工作效率,想象吧,你做1万道技术习题有什么用?只会让人觉得酸腐,还是在学习的时候,多培养些自己务 实的态度吧,比如研究一下当地市场目前有哪些软件公司用人,自己离他们的要求到底有多远,自己具体应该怎么做才可以达到他们的要求。等你分析完这些,你就 会发现,找工作成功,技术的贡献率其实并没有你原来想象的那么高。

不管你是学习技术为了找工作还是创业,你都要对技术本身有个清醒的认识,在中国不会出现BILL GATES,因为,中国目前还不是十分的尊重技术人才,还仅仅的停留在把软件技术人才当作人才机器来用的尴尬境地。(如果你不理解,一种可能是你目前仅仅 从事过技术工作,你的朋友圈子里技术类的朋友占了大多数,一种可能是你还没有工作,但喜欢读比尔。盖茨的传记)。

2006年09月18日

第7章       线程调度、优先级、亲缘性

1. 线程切换       切换时,windows选择可调度线程内核对象中的一个,将它加载到cpu寄存器中,其值是上次保存在线程环境中的值,这项操作称为上下文转换。环境结构使系统能记住线程状态,这样当下次得到cpu时间时能顺利找到上次中断运行的地方。用来保存线程环境值的结构为CONTEXT结构,它是特定于cpu的唯一数据结构Windows抢占式多线程操作系统,不具有实时性,所以无法保证线程在某个时间段内开始执行。

2. 暂停和恢复   在线程内核对象被创建时,其暂停计数为1,然后检查是否传递标志CREATE_SUSPEND。如无,递减计数,如有,保持1函数返回。也可使用SuspendThread来暂停线程。一个线程可调用该函数多次实现多次自己或其他线程的暂停。调用该函数必须非常小心,因为不知道暂停线程运行时它在进行什么。恢复线程运行ResumeThread函数。SuspendThreadResumeThread均返回线程的前一个暂停计数。

3. 进程的暂停   windows来说,不存在暂停和恢复进程的概念Windows允许一个进程暂停另一个进程中的所有线程运行,但是从事暂停操作的进程必须是个调试程序。尽管可以使用CreateToolhelp32Snapshot枚举线程以逐个暂停,但由于枚举过程中可能会有线程产生和撤消,因此不存在SuspendThread函数。

4. Sleep函数      可是线程自愿放弃其时间片并在指定ms数内不可调度。将0作为参数时,调用线程将释放剩余时间片并迫使系统重新调度。因为windows不具实时性,所以在指定ms到达后,线程可能被重新唤醒,也可能不会,但是其处于可调度状态。

5. SwitchToThreadSleep(0) 系统查看是否有迫切需要资源的线程,如有,则进行切换,没有则返回false。这个迫切需要cpu的线程运行一段时间后,系统调度程序照常运行。与sleep(0)的差别是:SwitchToThread允许优先级较低但迫切需要cpu时间的线程运行,Sleep(0)允许系统立即对调用线程重新进行调度

6. GetThreadTimes用于返回线程运行时间。GetProcessTimes用于返回进程中所有线程的运行时间。这些运行时间包括创建时间、退出时间、内核时间与用户时间

7. 优先级  每个线程都会被赋予一个0-31的优先级号码。高优先级线程抢在低优先级前运行。对同一优先级线程,系统以循环方式对他们进行调度。线程优先级由进程优先级类和相对线程优先级综合决定。有6个优先级类:实时、高、高于正常、正常、低于正常、空闲。相对的线程优先级:关键时间、最高、高于正常、正常、低于正常、最低、空闲。

8. 0页线程和实时    当系统引导时,会创建一个特殊线程,为0页线程,被赋予优先级0是整个系统中唯一一个在优先级0上运行的线程。当系统中没有任何线程需要执行操作时,0页线程负责将系统中所有空闲的ram也面置0应该尽可能避免使用实时优先级类,因为这可能会干扰操作系统任务的运行。阻止必要的磁盘I/O和网络信息的产生。

9. 优先级改变   可在CreateProcess时设置优先级类。也可创建后调用SetPriorityClass设置自己或其他进程的优先级类。但是在CreateThread时并不允许设置线程优先级,CreateThread函数创建的线程优先级总为正常优先级。可调用SetThreadPriority设置。

10.动态改变优先级  系统允许动态改变线程优先级,以便及时对窗口消息或读取磁盘I/O等作出响应。1导致线程成为可调度线程的设备驱动程序可以决定线程优先级提高的数量。2)当线程出现饥饿时,系统会将该线程优先级动态提高到15,执行两个时间片后再返回到基本优先级。动态优先级范围:系统只能为1-15的线程提高其优先级。但系统不允许将优先级提高到实时范围,因为这可能会干扰操作系统的运行。

11.亲缘性  按默认设置,当将线程分配个cpu时,win2k使用软亲缘性来操作,即如果所有其他因素相同,将设法在它上次运行的那个处理器上运行线程,让线程留在单个处理器上,有助于重复使用仍然在处理器的告诉缓存上的数据。可通过设置进程和线程的软亲缘性以限制在哪个cpu上运行,这称为硬亲缘性。函数为SetProcessAffinityMaskSetThreadAffinityMask。此外可以使用作业内核对象以限制一组进程在一组cpu上运行。也可以SetThreadIdealProcessor为线程设置理想cpu,即争取在该cpu上运行。

2006年09月17日

5 作业  &&  6 线程

1.作业 作业能将进程组合在一起,并且创建一个“沙框”,限制进程能够进行的操作。创建包含单个进程的作业同样有用。服务器段也经常创建作业来限制客户端请求创建的进程的资源安全性等。也可通过终止作业对象的运行来终止作业中所有进程的运行。

2.使用作业对象       通常分为如下步骤:

1)创建作业对象(关闭作业对象句柄并不会使作业中所有进程终止运行,该作业对象实际上做了删除标记,只有当作业中的所有进程终止运行时,该作业对象才被撤消)

2)对作业对象进行限制     包括基本与扩展基本、UI、安全性等限制。利用函数SetInformationJobObject实现。

3)将进程放入作业     调用CreateProcess函数创建进程,并赋予CREATE_SUSPEND参数。然后通过函数AssignProcessToJobObject来实现放入。

3  线程  由两部分组成:1)线程内核对象 2)线程堆栈 用于维护线程在执行代码时需要的所有函数参数和局部变量。同一进程中的多个线程共享地址空间,也能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程而存在。

4.用户界面线程  通常一个进程拥有一个用户界面线程,用于创建窗口的所有子窗口,并且有一个GetMessage循环。其他线程都是优先级较低的工作线程,正因如此,用户界面线程负责向用户响应。但也有例外,如Explorer为每个文件夹窗口创建一个独立线程,这使得能从一个文件夹拷贝文件到另一个文件夹。

5.创建线程 非主线程的进入点函数可随意命名,但必须返回值作为线程退出代码,且类型为WINAPI类型。新创建的线程与主线程的进程环境相同。可以访问进程内核对象的所有句柄,进程中的所有内存和同进程的其他线程堆栈。另外,使用相同的线程函数来创建多个线程也非常有用,如服务器就经常如此处理多个客户机的请求。

6.终止线程的运行有如下四种方式

       1)线程函数返回  正确返回将执行如下操作

n         线程堆栈能被操作系统正确释放

n         线程函数中创建的所有C++对象都将被正确析构

n         线程函数返回值即为线程的退出代码

n         系统将递减线程内核对象的使用计数      

       2VOID ExitThread(DWORD dwExitCode)函数 终止自己的运行

n         线程资源能被系统清除

n         C++资源将不被撤消

n         线程退出代码被设置为dwExitCode

n         内核对象使用计数被递减

n         编译器供应商有自己的替代函数,如_endthreadex

       3) TerminateThread函数 ExitThread区别为:

n         它能终止任何线程的运行

n         线程被终止时得不到任何它被撤消的通知

n         在进程终止前,不撤消线程堆栈(此为MS公司的刻意设计,避免访问违规现象。如th1正访问th3堆栈,此时th2终止th3th2th3得不到任何通知,如果撤消th3的堆栈,则访问违规)

       4) 在进程终止运行时撤消线程  进程被终止,进程所有资源被清除,也包括线程堆栈。

7.线程终止运行时的情况

n         线程拥有的用户对象被释放(大多数由包含创建对象的线程的进程所拥有,但窗口和挂钩也可为线程所拥有),其他用户对象在进程被撤消时释放。

n         线程的退出代码从STILL_ACTIVE改为退出函数设置的值。

n         线程内核对象变为已通知状态,使用计数减1

n         如果线程为进程最后一个活动线程,则系统也将终止进程

3.   线程一些性质     

1)创建线程首先创建线程内核对象,包括线程上下文和其属性统计信息。并见暂停计数设为1。当线程初始化后,依据是否传递CREATE_SUSPEND标志决定是否递减暂停计数。当暂停计数为0,该线程就可调度。

2BaseThreadStart并不由别的函数调用,而是新线程在此处产生并开始执行。该函数的两个参数是操作系统将值显式写入线程堆栈中。另外在该函数中,要么调用ExitThread要么调用ExitProcess,也就是说线程总是在函数中被撤消,该函数从来不会返回。而且线程函数堆栈上也不存在该函数的返回地址,因为该函数不是被其他函数所调用的,只是个执行入口。

9C/C++运行期库的线程考虑       由于C标准运行期库没有考虑到多线程问题,C/C++运行期库包含的全局变量和函数errno等会因线程异步而产生问题,因此,必须创建一个数据结构存储这些全局变量与函数,并将它与使用C/C++运行期库函数的每个线程关联起来,避免一个线程进行变量访问对另一个线程产生影响。

10_beginthreadex_endthreadex       这函数只存于C/C++多线程版本中。_beginthreadex内部调用CreateThread函数。_beginthreadex_threadstartex_endthreadex分别为CreateThread、线程函数、ExitThread的外封装。由于这些附加操作会影响多线程版本的C/C++运行期库性能。所以,MS还提供了单线程的静态链接C/C++运行期库版本。

11.伪句柄   GetCurrentProcessGetCurrentThread函数返回伪句柄,即并不在进程句柄表中创建,其使用计数也不增加。只能表示当前进程或线程的句柄,如果用线程伪句柄作函数参数,则句柄会发生变化,实际表示的为调用该函数的线程,非原来GetCurrentThread所得。可用DuplicateHandle将伪句柄变成实句柄。注意在DuplicateHandle后应CloseHandle