• Windows核心编程 第六章 线程基础知识 (下)


    6.6 线程的一些性质

        到现在为止,讲述了如何实现线程函数和如何让系统创建线程以便执行该函数。本节将要介绍系统如何使这些操作获得成功。

        图6 - 1显示了系统在创建线程和对线程进行初始化时必须做些什么工作。让我们仔细看一看这个图,以便确切地了解发生的具体情况。调用 C r e a t e T h r e a d可使系统创建一个线程内核对象。该对象的初始使用计数是2(在线程停止运行和从C r e a t e T h r e a d返回的句柄关闭之前,线程内核对象不会被撤消) 。线程的内核对象的其他属性也被初始化,暂停计数被设置为 1,退出代

    码始终为S T I L L _ A C T I V E0 x 1 0 3) ,该对象设置为未通知状态。

     

        一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。该内存是从进程的地址空间分配而来的,因为线程并不拥有它自己的地址空间。然后系统将两个值写入新线程的堆栈的上端(线程堆栈总是从内存的高地址向低地址建立) 。写入堆栈的第一个值是传递给C r e a t e T h r e a dp v P a r a m参数的值。紧靠它的下面是传递给C r e a t e T h r e a dp f n S t a r t A d d r参数的值。

        每个线程都有它自己的一组C P U寄存器,称为线程的上下文。该上下文反映了线程上次运时该线程的 C P U寄存器的状态。线程的这组 C P U寄存器保存在一个 C O N T E X T结构(在Wi n N T. h头文件中作了定义)中。C O N T E X T结构本身则包含在线程的内核对象中。

    指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。记住,线程总是在进程的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。当线程的内核对象被初始化时,C O N T E X T结构的堆栈指针寄存器被设置为线程堆栈上用来放置p f n S t a r t -A d d r的地址。指令指针寄存器置为称为B a s e T h r e a d S t a r t的未文档化(和未输出)的函数的地址中。该函数包含在K e r n e l 3 2 . d l l模块中(这也是实现C r e a t e T h r e a d函数的地方) 。图6 - 1显示了它的全部情况。

    下面是B a s e T h r e a d S t a r t函数执行的基本操作:

     

        当线程完全初始化后,系统就要查看 C R E AT E _ S U S P E N D E D标志是否已经传递给C r e a t e T h r e a d。如果该标志没有传递,系统便将线程的暂停计数递减为 0,该线程可以调度到一个进程中。然后系统用上次保存在线程上下文中的值加载到实际的 C P U寄存器中。这时线程就可以执行代码,并对它的进程的地址空间中的数据进行操作。

        由于新线程的指令指针被置为B a s e T h r e a d S t a r t,因此该函数实际上是线程开始执行的地方。B a s e T h r e a d S t a r t的原型会使你认为该函数接收了两个参数,但是这表示该函数是由另一个函数来调用的,而实际情况并非如此。新线程只是在此处产生并且开始执行。 B a s e T h r e a d S t a r t认为它是由另一个函数调用的,因为它可以访问两个函数。但是,之所以可以访问这些参数,是因为操作系统将值显式写入了线程的堆栈(这就是参数通常传递给函数的方法) 。注意,有些C P U结构使用C P U寄存器而不是堆栈来传递参数。对于这些结构来说,系统将在允许线程执行B a s e T h r e a d S t a r t函数之前对相应的寄存器正确地进行初始化。

        当新线程执行B a s e T h r e a d S t a r t函数时,将会出现下列情况:

    • 在线程函数中建立一个结构化异常处理(S E H)帧,这样,在线程执行时产生的任何异常情况都会得到系统的某种默认处理(关于结构化异常处理的详细说明参见第 2 32 42 5章) 。

    • 系统调用线程函数,并将你传递给C r e a t e T h r e a d函数的p v P a r a m参数传递给它。

    • 当线程函数返回时,B a s e T h r e a d S t a r t调用E x i t T h r e a d,并将线程函数的返回值传递给它。该线程内核对象的使用计数被递减,线程停止执行。

    • 如果线程产生一个没有处理的异常条件,由B a s e T h r e a d S t a r t函数建立的S E H帧将负责处理该异常条件。通常情况下,这意味着向用户显示一个消息框,并且在用户撤消该消息框时,BzsethreadStart调用ExitThread,以终止整个进程的运行,而不只是终止线程的运行。

    注意,在B a s e T h r e a d S t a r t函数中,线程要么调用E x i t T h r e a d,要么调用E x i t P r o c e s s。这意味着线程不能退出该函数,它总是在函数中被撤消。这就是 B a s e T h r e a d S t a r t的原型规定返回V O I D,而它从来不返回的原因。

        另外,由于使用B a s e T h r e a d S t a r t,线程函数可以在它完成处理后返回。当 B a s e T h r e a d S t a r t调用线程函数时,它会把返回地址推进堆栈,这样,线程函数就能知道在何处返回。但是,B a s e T h r e a d S t a r t不允许返回。如果它不强制撤消线程,而只是试图返回,那么几乎可以肯定会引发访问违规,因为线程堆栈上不存在返回地址,并且 B a s e T h r e a d S t a r t将试图返回到某个随机内存位置。

    当进程的主线程被初始化时,它的指令指针被设置为另一个未文档化的函数,称为B a s e P r o c e s s S t a r t。该函数几乎与B a s e T h r e a d S t a r t相同,形式类似下面的样子:

     

         这两个函数之间的唯一差别是,B a s e P r o c e s s S t a r t没有引用p v P a r a m参数。当B a s e P r o c e s s S t a r t开始执行时,它调用 C / C + +运行期库的启动代码,该启动代码先要初始化 m a i nw m a i nWi n M a i nw Wi n M a i n函数,然后调用这些函数。当进入点函数返回时, C / C + +运行期库的启动代码就调用E x i t P r o c e s s。因此,对于C / C + +应用程序来说,主线程从不返回B a s e P r o c e s s S t a r t函数。

    6.7 C/C++运行期库的考虑

    Visual C++配有6C / C + +运行期库。表6 - 1对它们进行了描述。

     

       当实现任何类型的编程项目时,必须知道将哪个库与你的项目相链接。可以使用图 6 - 2所示的Project Settings对话框来选定一个库。在C / C + +选项卡上,在Code Generation(生成的代码)类别中,从Use run-time library(使用运行期库)组合框中选定6个选项中的一个。

     

        应该考虑的第一件事情是, “为什么必须将一个库用于单线程应用程序,而将另一个库用于多线程应用程序?” ,原因是,标准C运行期库是1 9 7 0年问世的,它远远早于线程在任何应用程序上的应用。运行期库的发明者没有考虑到将C运行期库用于多线程应用程序的问题。

        考虑一下标准C运行期的全局变量e r r n o。有些函数在发生错误时设置该变量。假设拥有下面这个代码段:

     

        现在,假设在调用s y s t e m函数之后和调用i f语句之前,执行上面代码的线程中断运行,同时假设,该线程中断运行是为了让同一进程中的第二个线程开始执行,而这个新线程将执行另一个负责设置全局变量 e r r n oC运行期函数。当 C P U在晚些时候重新分配给第一个线程时,e r r n o的值将不再能够反映调用上面代码中的s y s t e m函数时的错误代码。为了解决这个问题,每个线程都需要它自己的 e r r n o变量。此外,必须有一种机制,使得线程能够引用它自己的 e r r n o变量,但是又不触及另一个线程的e r r n o变量。

        这是标准C / C + +运行期库原先并不是设计用于多线程应用程序的唯一一个例子。在多线程环境中存在问题的C / C + +运行期库变量和函数包括e r r n o_ d o s e r r n os t r t o k_ w c s t o ks t r e r r o r_ s t r e r r o rt m p n a mt m p f i l ea s c t i m e_ w a s c t i m eg m t i m e_ e c v t_ f c v t等。

        若要使多线程 CC + +程序能够正确地运行,必须创建一个数据结构,并将它与使用C / C + +运行期库函数的每个线程关联起来。当你调用 C / C + +运行期库时,这些函数必须知道查看调用线程的数据块,这样就不会对别的线程产生不良影响。

    那么系统是否知道在创建新线程时分配该数据块呢?回答是它不知道。系统根本不知道你得到的应用程序是用C / C + +编写的,也不知道你调用函数的线程本身是不安全的。问题在于你必须正确地进行所有的操作。若要创建一个新线程,绝对不要调用操作系统的 C r e a t e T h r e a d函数,必须调用C / C + +运行期库函数_ b e g i n t h r e a d e x

     

    _ b e g i n t h r e a d e x函数的参数列表与C r e a t e T h r e a d函数的参数列表是相同的,但是参数名和类型并不完全相同。这是因为M i c r o s o f tC / C + +运行期库的开发小组认为,C / C + +运行期函数不应该对Wi n d o w s数据类型有任何依赖。_ b e g i n t h r e a d e x函数也像C r e a t e T h r e a d那样,返回新创建的线程的句柄。因此,如果调用源代码中的 C r e a t e T h r e a d,就很容易用对_ b e g i n t h r e a d e x的调用全局取代所有这些调用。不过,由于数据类型并不完全相同,所以必须进行某种转换,使编译器运行得顺利些。为了使操作更加容易,我在源代码中创建了一个宏 c h B E G I N T H R E A D E X

     

        注意,_ b e g i n t h r e a d e x函数只存在于C / C + +运行期库的多线程版本中。如果链接到单线程运行期库,就会得到一个链接程序报告的“未转换的外部符号”错误消息。当然,从设计上讲,这个错误的原因是单线程库在多线程应用程序中不能正确地运行。另外需要注意,当创建一个新项目时,Visual Studio默认选定单线程库。这并不是最安全的默认设置,对于多线程应用程序来说,必须显式转换到多线程的C / C + +运行期库。

        由于M i c r o s o f tC / C + +运行期库提供了源代码,因此很容易准确地确定 C r e a t e T h r e a d究竟无法执行哪些 _ b e g i n t h r e a d e x能执行的操作。实际上,我搜索了 Visual Studio的光盘,发现_ b e g i n t h r e a d e x的源代码在T h r e a d e x . c中。代换重新打印它的源代码,这里提供了它的伪代码版本,并且列出它的一些令人感兴趣的要点:

     

        下面是关于_ b e g i n t h r e a d e x的一些要点:

    • 每个线程均获得由C / C + +运行期库的堆栈分配的自己的 t i d d a t a内存结构。 (t i d d a t a结构位于M t d l l . h文件中的Visual C++源代码中) 。我在清单6 - 1中重建了它的结构。

    • 传递给_ b e g i n t h r e a d e x的线程函数的地址保存在t i d d a t a内存块中。传递给该函数的参数也保存在该数据块中。

    _ b e g i n t h r e a d e x确实从内部调用C r e a t e T h r e a d,因为这是操作系统了解如何创建新线程的唯一方法。

    • 当调用C r e a t e t T h r e a d时,它被告知通过调用_ t h r e a d s t a r t e x而不是p f n S t a r t A d d r来启动执行新线程。还有,传递给线程函数的参数是t i d d a t a结构而不是p v P a r a m的地址。

    • 如果一切顺利,就会像 C r e a t e T h r e a d那样返回线程句柄。如果任何操作失败了,便返回N U L L

    既然为新线程指定了t i d d a t a结构,并且对该结构进行了初始化,那么必须了解该结构与线程之间是如何关联起来的。让我们观察一下 _ t h r e a d s t a r t e x函数(它也位于 C / C + +运行期库的T h r e a d e x . c文件中) 。这里是该函数的伪代码版本:

     

        下面是关于_ t h r e a d s t a r t e x的一些重点:

    • 新线程开始从 B a s e t h r e a d S t a r t函数(在 k e r n e l 3 2 . d l l文件中)执行,然后转移到_ t h r e a d s t a r t e x

    • 到达该新线程的t i d d a t a块的地址作为其唯一参数被传递给_ t h r e a d s t a r t e x

    T l s S e t Va l u e是个操作系统函数,负责将一个值与调用线程联系起来。这称为线程本地存储器(T L S) ,将在第2 1章介绍。_ t h r e a d s t a r t e x函数将t i d d a t a块与线程联系起来。

    • 一个S E H帧被放置在需要的线程函数周围。这个帧负责处理与运行期库相关的许多事情— 例如,运行期错误(比如放过了没有抓住的 C + +异常条件)和 C / C + +运行期库的s i g n a l函数。这是特别重要的。如果用 C r e a t e T h r e a d函数来创建线程,然后调用 C / C + +运行期库的s i g n a l函数,那么该函数就不能正确地运行。

    • 调用必要的线程函数,传递必要的参数。记住,函数和参数的地址由 _ b e g i n t h r e a d e x保存在t i d d a t a块中。

    • 必要的线程函数返回值被认为是线程的退出代码。

    注意, _ t h r e a d s t a r t e x并不只是返回到B a s e T h r e a d S t a r t。如果它准备这样做,那么线程就终止运行,它的退出代码将被正确地设置,但是线程的t i d d a t a内存块不会被撤消。这将导致应用程序中出现一个漏洞。若要防止这个漏洞,可以调用另一个C / C + +运行期库函数_ e n d t h r e a d e x ,并传递退出代码。需要介绍的最后一个函数是 _ e n d t h r e a d e x(位于C运行期库的T h r e a d e x . c文件中) 。下面是该函数的伪代码版本:

     

        下面是关于_ e n d t h r e a d e x的一些要点:

    C运行期库的_ g e t p t d函数内部调用操作系统的 T l s G e t Va l u e函数,该函数负责检索调用线程的t i d d a t a内存块的地址。

    • 然后该数据块被释放,而操作系统的E x i t T h r e a d函数被调用,以便真正撤消该线程。当然,退出代码要正确地设置和传递。

    本章前面说过,始终都应该设法避免使用 E x i t T h r e a d函数。这一点完全正确,我并不想收回我已经说过的话。ExitThread 函数将撤消调用函数,并且不允许它从当前执行的函数返回。由于该函数不能返回,所以创建的任何 C + +对象都不会被撤消。避免调用 E x i t T h r e a d的另一个原因是,它会使得线程的t i d d a t a内存块无法释放,这样,应用程序将会始终占用内存(直到整个进程终止运行为止) 。

    M i c r o s o f tVisual C++开发小组认识到编程人员喜欢调用 E x i t T h r e a d,因此他们实现了他们的愿望,并且不会让应用程序始终占用内存。如果真的想要强制撤消线程,可以让它调用_ e n d t h r e a d e x(而不是调用E x i t T h r e a d)以便释放线程的t i d d a t a块,然后退出。不过建议不要调用_ e n d t h r e a d e x函数。

    现在应该懂得为什么C / C + +运行期库的函数需要为它创建的每个线程设置单独的数据块,同时,也应该了解如何通过调用 _ b e g i n t h r e a d e x来分配数据块,再对它进行初始化,将该数据块与你创建的线程联系起来。你还应该懂得 _ e n d t h r e a d e x函数是如何在线程终止运行时释放数据块的。

    一旦数据块被初始化并且与线程联系起来,线程调用的任何需要单线程实例数据的 C / C + +运行期库函数都能很容易地(通过T l s G e t Va l u e)检索调用线程的数据块地址,并对线程的数据进行操作。这对于函数来说很好,但是你可能想知道它对 e r r n o之类的全局变量效果如何。E r r n o定义在标准的C头文件中,类似下面的形式:

     

        如果创建一个多线程应用程序,必须在编译器的命令行上设定 / M T(指多线程应用程序)或/ M D(指多线程D L L)开关。这将使编译器能够定义 _ M T标识符。然后,每当引用e r r n o时,实际上是调用内部的C / C + +运行期库函数_ e r r n o。该函数返回调用线程的相关数据块中的 e r r n o数据成员的地址。你将会发现, e r r n o宏被定义为获取该地址的内容的宏。这个定义是必要的,

    因为可以编写类似下面形式的代码:

     

        如果内部函数_ e r r n o只返回e r r n o的值,那么上面的代码将不进行编译。

    多线程版本的C / C + +运行期库还给某些函数设置了同步的基本要素。例如,如果两个线程同时调用m a l l o c,那么内存堆栈就可能遭到破坏。多线程版本的 C / C + +运行期库能够防止两个线程同时从堆栈中分配内存。为此,它要让第二个线程等待,直到第一个线程从 m a l l o c返回。然后第二个线程才被允许进入(关于线程同步的问题将在第 89章和1 0章详细介绍) 。

        显然,所有这些附加操作都会影响多线程版本的 C / C + +运行期库的性能。这就是为什么M i c r o s o f t公司除了多线程版本外,还提供单线程版本的静态链接的C / C + +运行期库的原因。

    C / C + +运行期库的动态连接版本编写成为一种通用版本。这样它就可以被使用 C / C + +运行期库函数的所有正在运行的应用程序和D L L共享。由于这个原因,运行期库只存在于多线程版本中。由于D L L中提供了C / C + +运行期库,因此应用程序(. e x e文件)和D L L不需要包含C / C + +运行期库函数的代码,结果它们的规模就比较小。另外,如果 M i c r o s o f t排除了C / C + +运行期库

    D L L中的错误,应用程序中的错误也会自动得到解决。

        正如希望的那样,C / C + +运行期库的启动代码为应用程序的主线程分配了数据块,并且对数据块进行了初始化,这样,主线程就能安全地调用 C / C + +运行期函数中的任何函数。当主线程从它的进入点函数返回时,C / C + +运行期库就会释放相关的数据块。此外,启动代码设置了相应的结构化异常处理代码,以便主线程能够成功地调用C / C + +运行期库的s i g n a l函数。

    6.7.1 Oops — 错误地调用了C r e a t e T h r e a d

    也许你想知道,如果调用C r e a t e T h r e a d,而不是调用C / C + +运行期库的_ b e g i n t h r e a d e x来创建新线程,将会发生什么情况。当一个线程调用要求 t i d d a t a结构的C / C + +运行期库函数时,将会发生下面的一些情况(大多数 C / C + +运行期库函数都是线程安全函数,不需要该结构) 。首先,C / C + +运行期库函数试图 (通过调用 T l s G e t Va l u e )获取线程的数据块的地址。如果返回N U L L作为t i d d a t a块的地址,调用线程就不拥有与该地址相关的t i d d a t a块。这时,C / C + +运行期库函数就在现场为调用线程分配一个 t i d d a t a块,并对它进行初始化。然后该 t i d d a t a块(通过T l s S e t Va l u e)与线程相关联。此时,只要线程在运行,该 t i d d a t a将与线程待在一起。这时,C / C + +运行期库函数就可以使用线程的 t i d d a t a块,而且将来被调用的所有 C / C + +运行期函数也能使用t i d d a t a块。

    当然,这看来有些奇怪,因为线程运行时几乎没有任何障碍。不过,实际上还是存在一些问题。首先,如果线程使用 C / C + +运行期库的s i g n a l函数,那么整个进程就会终止运行,因为结构化异常处理帧尚未准备好。第二,如果不是调用 _ e n d t h r e a d e x来终止线程的运行,那么数据块就不会被撤消,内存泄漏就会出现(那么谁还为使用 C r e a t e T h r e a d函数创建的线程来调用_ e n d t h r e a d e x呢?) 。

    注意 如果程序模块链接到多线程D L L版本的C / C + +运行期库,那么当线程终止运行并释放 t i d d a t a块(如果已经分配了 t i d d a t a块的话)时,该运行期库会收到一个D L L _ T H R E A D _ D E TA C H通知。尽管这可以防止t i d d a t a块的泄漏,但是强烈建议使用_ b d g i n t h r e a d e x而不是使用C r e a t e t h r e a d来创建线程。

     

    6.7.2 不应该调用的C / C + +运行期库函数

    C / C + +运行期库也包含另外两个函数:

    unsigned long _begininthread()void _endthread(void);

       创建这两个函数的目的是用来执行_ b e g i n t h r e a d e x_ e n d t h r e a d e x函数的功能。但是,如你所见,_ b e g i n t h r e a d函数的参数比较少,因此比特性全面的_ b e g i n t h r e a d e x函数受到更大的限制。例如,如果使用_ b e g i n t h r e a d,就无法创建带有安全属性的新线程,无法创建暂停的线程,也

    无法获得线程的I D值。_ e n d t h r e a d函数的情况与之类似。它不带参数,这意味着线程的退出代码必须硬编码为0

    e n d t h r e a d函数还存在另一个很难注意到的大问题。在_ e n d t h r e a d调用E x i t T h r e a d之前,它调用C l o s e H a n d l e,传递新线程的句柄。若要了解为什么这是个大问题,请看下面的代码:

     

    新创建的线程可以在第一个线程调用G e t E x i t C o d e T h r e a d之前运行、返回和终止。如果出现这种情况,h T h r e a d中的值将无效,因为_ e n d t h r e a d已经关闭了新线程的句柄。不用说,由于相同的原因,对C l o s e H a n d l e的调用也将失败。

    新的_ e n d t h r e a d e x函数并不关闭线程的句柄,因此,如果用调用 b e g i n t h r e a d e x来取代调用_ b e g i n t h r e a d,那么上面的代码段将能正确运行。记住,当线程函数返回时, _ b e g i n t h r t e a d e x调用_ e n d t h r e a d e x,而_ b e g i n t h r e a d则调用_ e n d t h r e a d

    6.8 对自己的I D概念应该有所了解

        当线程运行时,它们常常想要调用Wi n d o w s函数来改变它们的运行环境。例如,线程可能想要改变它的优先级或它的进程的优先级(优先级将在第 7章中介绍) 。由于线程常常要改变它的(或它的进程的)环境,因此Wi n d o w s提供了一些函数,使线程能够很容易引用它的进程内核对象,或者引用它自己的线程内核对象:

    DWORD GetCurrentProcessId();

    DWORD GetCurrentThreadId();

        上面这两个函数都能返回调用线程的进程的伪句柄或线程内核对象的伪句柄。这些函数并不在创建进程的句柄表中创建新句柄。还有,调用这些函数对进程或线程内核对象的使用计数没有任何影响。如果调用 C l o s e H a n d l e,将伪句柄作为参数来传递,那么 C l o s e H a n d l e就会忽略该函数的调用并返回FA L S E

        当调用一个需要进程句柄或线程句柄的 Wi n d o w s函数时,可以传递一个伪句柄,使该函数执行它对调用进程或线程的操作。例如,通过调用下面的 G e t P r o c e s s Ti m e s函数,线程可以查询它的进程的时间使用情况:

     

        同样,通过调用G e t T h r e a d Ti m e s函数,线程可以查询它自己的线程时间:

     

        少数Wi n d o w s函数允许用进程或线程在系统范围内独一无二的 I D来标识某个进程或线程。

    下面这两个函数使得线程能够查询它的进程的唯一I D或它自己的唯一I D

    DWORD GetCurrentProcessId();

    DWORD GetCurrentThreadId();

    这两个函数通常不像能够返回伪句柄的函数那样有用,但是有的时候用起来还是很方便的。

    将伪句柄转换为实句柄

    有时可能需要获得线程的实句柄而不是它的伪句柄。所谓“实句柄” ,我是指用来明确标识一个独一无二的线程的句柄。请看下面的代码:

     

        你能发现这个代码段存在的问题吗?这个代码的目的是让父线程给子线程传递一个线程句柄,以标识父线程。但是,父线程传递了一个伪句柄,而不是一个实句柄。当子线程开始运行时,它将一个伪句柄传递给 G e t T h r e a d Ti m e函数,使子线程得到它自己的 C P U时间,而不是父线程的C P U时间。出现这种情况的原因是线程的伪句柄是当前线程的句柄,也就是说,它是调用函数的线程的句柄。

    为了修改这个代码,必须将伪句柄变成实句柄。D u p l i c a t e H a n d l e函数能够执行这一转换:

     

    通常可以使用这个函数, 用与另一个进程相关的内核对象来创建一个与进程相关的新句柄。然而,可以用一种特殊的方法来使用这个函数,以便修改上面介绍的代码段。正确的代码段应该是下面的样子:

     


        当父线程运行时,它就将标识父线程所用的伪句柄转换成明确标识父线程所用的新的实句柄。同时它将这个实句柄传递    给C r e a t e T h r e a d。当子线程启动运行时,它的p v P a r a m参数包含了线程的实句柄。对传递该句柄的函数的任何调用都将影响父线程而不是子线程。

        由于D u p l i c a t e H a n d l e会递增特定对象的使用计数,因此当完成对复制对象句柄的使用时,应该将目标句柄传递给 C l o s e H a n d l e,从而递减对象的使用计数,这一点很重要。上面的代码段已经显示出这一点。在调用G e t T h r e a d Ti m e s之后,紧接着子线程调用C l o s e H a n d l e,以便递减父线程对象的使用计数。在这个代码段中,我假设子线程不使用该句柄来调用任何其他函数。如果其他函数被调用,以便传递父线程的句柄,那么在子线程不再需要该句柄之前,不应该调用C l o s e H a n d l e.

        还要指出,D u p l i c a t e H a n d l e可以用来将进程的伪句柄转换成进程的实句柄,如下面的代码所示:


  • 相关阅读:
    [Swift通天遁地]五、高级扩展-(2)扩展集合类型
    [Swift通天遁地]五、高级扩展-(1)快速检测设备属性:版本、类型、屏幕尺寸
    [Swift]LeetCode266.回文全排列 $ Palindrome Permutation
    [Swift]LeetCode265.粉刷房子 II $ Paint House II
    [Swift]LeetCode264.丑数 II | Ugly Number II
    [Swift通天遁地]四、网络和线程-(15)程序内购功能
    [Swift通天遁地]四、网络和线程-(14)创建一个Socket服务端
    hdu 4888 Redraw Beautiful Drawings(最大流,判环)
    【剑指offer】斐波那契序列与跳台阶
    Asp.NET之对象学习
  • 原文地址:https://www.cnblogs.com/csnd/p/12062251.html
Copyright © 2020-2023  润新知