非常实用的VC中线程使用说明

来源:互联网 发布:yum缓存目录 编辑:程序博客网 时间:2024/06/05 04:09

【致谢】http://blog.chinaunix.net/uid-14607221-id-2794653.html

【致谢】http://www.cnblogs.com/berry/archive/2009/10/09/1579942.html

VC中创建线程分析

1、CreateThread、_beginthreadex、AfxBeginThread的区别和正确使用:

CreateThread是一个Windows 的API函数,_beginthreadex是一个微软VC中C运行时库中的线程创建函数,AfxBeginThread则是MFC中的线程创建函数。

其依赖关系为:<--表示被依赖

CreateThread <--_beginthreadex

CreateThread <-- AfxBeginThread

_beginthreadex为每个使用线程在Heap上创建(用__calloc_crt,相当于calloc)了一个tiddata结构并且设定到动态TLS。这样C运行时库中使用静态变量的几个函数就可以得到只和线程相关的一份“静态”和“全局”变量了,C运行时全局变量也不会互相干扰。假若使用CreateThread创建了这同样的线程,因为没有事先分配这个tiddata结构,那么后果自然就很严重了。

但是使用了_beginthreadex也有一个不算麻烦的小麻烦。

线程最好的退出方式是从return退出,所有的资源都会正确的释放,如果调用ExitThread退出线程,那么线程堆栈上的内容被自动清除,但是C++对象不能够通过调用析构函数而正确的清除。而和_beginthreadex对应的_endthreadex恰恰就是调用了ExitThread来终止线程。所以,这下_endthreadex也不能调用了。那么在前面分配的tiddata怎么办?没有任何办法,除非去调用_endthreadex才会得到释放,但是ExitThread语句又紧跟在后边。还好,tiddata结构非常小,不超过100个字节,但是要是遇上傻大黑粗的野蛮程序员,在进程生命期中反复创建线程,也挺恐怖。

所以最好的做法就是,在线程中,避免使用C运行时函数,用C++运行时库或者Windows API解决问题。这样就可以避免使用_beginthreadex,而用CreateThread创建线程,也不会有tiddata结构的运行时内存泄漏了。(或者,不在线程中使用C++对象,这样就不怕_endthreadex的ExitThread 了。吐舌 馊主意!不过_beginthreadex本来就是为使用C函数的线程准备的。)

至于AfxBeginThread,是MFC中定义的一个函数,如果你使用MFC框架,那么应该使用这个函数,这样更符合框架的设计目的。这个函数内部的实现就是通过CreateThread,因为既然都使用MFC了,还费劲使用C函数库干嘛?

2、CreateThread的不知道算不算Bug的Bug:

CreateThread的定义请参见不同版本的MSDN。

这个地址是VS2008 MSDN中的定义 http://msdn.microsoft.com/en-us/library/ms682453(VS.85).aspx

该API函数的第2个参数 dwStackSize 以及 第5个参数 dwCreationFlag 分别指定了线程堆栈的大小和创建标志。

按照文档中的说明,dwCreationFlag有一个设定值为STACK_SIZE_PARAM_IS_A_RESERVATION。这个标志的含义是,将指定大小的堆栈在虚拟内存中分配,并且初始只映射系统缺省页数的物理内存(大概是两个页),随着堆栈使用的增大,系统根据堆栈物理内存页后方的设定为PAGE_GUARD属性的页触发来把更多的物理内存映射到后续的页。

值得注意的是,STACK_SIZE_PARAM_IS_A_RESERVATION这个标志只在Windows XP以及Windows 2003以及更新的系统上才会得到支持。如果没有设定这个标志,并且也让线程一创建就开始运行(大部分时候是这样),那么系统会参照dwStackSize中指定的堆栈大小,并参照内存分配最小粒度以及页对齐原则,把一定大小的物理内存映射给堆栈。

看上去一切都很不错,但是如果你尝试创建一个会很快耗光堆栈的线程ThreadProc,并且用下面的语句创建:

CreateThread(NULL, 1024 * 1024 /* 1MB */, ThreadProc, NULL, 0, &threadId);

然后在线程中精心设置好异常捕捉,满心欢喜的等待EXCEPTION_STACK_OVERFLOW的到来。

那么你会震惊的发现:

程序因为EXCEPTION_ACCESS_VIOLATION异常而直接退出了。没有EXCEPTION_STACK_OVERFLOW。

你重新设置异常条件,决定捕捉全部的异常,但是你会发现你完全无法捕捉。Why?

接下来,你把堆栈大小设置为:1024 * 1024 + 1 或者1024 * 1024 - 8092,那么又可以准确的捕捉到溢出异常了。

或者你把dwStackSize设置为0,或者给dwCreationFlag 加上STACK_SIZE_PARAM_IS_A_RESERVATION标志,也可以成功捕捉异常。

现在应该知道原因了。运行在x86上的Windows XP,目前内存分配最小粒度为64KB,用户态下页面大小是4KB。

当把dwStackSize设置为0时,系统会缺省设置堆栈为1MB大小,但是事实上线程得不到1MB的堆栈,因为系统会在堆栈最后的部分用加上PAGE_GUARD属性的页来检测溢出,并且还要保留用于处理溢出异常发生时的页面,所以可以检测到异常。

而给dwCreationFlag 加上STACK_SIZE_PARAM_IS_A_RESERVATION标志时,按照64KB分配粒度的原则,堆栈最后的部分始终有足够的页来检测溢出和处理异常。所以也很安全。

而为线程指定1024 * 1024大小堆栈时,堆栈边界正好和64KB以及1MB边界对齐的,当完全提交物理内存时,堆栈的所有页的属性都将是PAGE_READWRITE(事实上应该是一个Windows的Bug,但是考虑性能原因而故意让其存在)。所以系统根本没机会检测到溢出错误,而会在堆栈溢出后直接引发EXCEPTION_ACCESS_VIOLATION。而因为没有预留的异常处理页面,异常处理程序又会引发一个EXCEPTION_ACCESS_VIOLATION异常。天啦!!整个进程就这样崩溃了。吐舌

而为什么指定1024 * 1024 + 1 或者1024 * 1024 - 8092就可以捕捉异常了呢?因为堆栈大小在超过1MB后,是按照1MB对齐原则,哪怕多一个字节,系统也会多映射1MB的物理内存。这样足够两页的异常检测和异常处理页面。1024 * 1024 - 8092大小正是这个原因所以能正确引发异常。

所以,正确的做法是,如果想在运行CreateThread时指定堆栈大小,并且全部提交物理内存的话,那么大小必须保证在整1MB边界上留出至少两个页面给系统。

 

vc 线程创建和关闭

★两套API :OS API vs CRT API

  本来照例要先介绍线程的几种死法,但是考虑到很多Windows程序员经常混淆线程API,搞不清楚到底该用哪个。所以先来说一下两套线程API的问题。

  首先,Windows操作系统本身提供了线程的创建函数CreateThread 和销毁函数ExitThread 。其中的CreateThread 用于创建线程,ExitThread 用于在线程函数内部推出线程(也就是自杀)。

  其次,在Visual C++自带的C运行库(以下简称CRT)中,还带了另外4个API函数,分别是:_beginthread ,_endthread ,_beginthreadex ,_endthreadex 。其中的_beginthread 和_beginthreadex 用于创建线程(它们内部调用了CreateThread ),_endthread 和_endthreadex 用于自杀(它们内部调用了ExitThread )。

  有同学看到这里,被搞懵了,心想:“干嘛要搞这么多玩意儿出来糊弄人?有CreateThread 和ExitThread 不就够了嘛!”其实你有所不知,此中大有奥妙啊。

  因为OS API作为操作系统本身提供的API函数,它被设计为语言无关的。它们不光可以被C++调用,还可以被其它诸如VB、Python、Delphi等开发语言来调用。所以它们不会(也不能够)帮你处理一些和具体编程语言相关的琐事。

  而CRT API虽然最终还是要调用OS API来完成核心的功能,但是CRT API在不知不觉中多帮我们干了一些虽琐碎但重要的工作。(如果同学们想窥探一下CRT API内部都干了些啥,可以拜读一下Win32编程的经典名着《Windows 核心编程》的6.7 章节,里面介绍得挺细致的)

  费了这么多口水,无非是要同学们牢记:以后在Windows平台下开发多线程程序,千万不要 直接使用这两个线程API(也就是CreateThread 和ExitThread ),否则后果自负 :-)

  另外,顺便补充一下。除了上述提到的CRT库。其它一些Windows平台的C++库也可能提供了线程的启动函数(比如MFC的AfxBeginThread),这些函数也对OS API进行了包装,所以用起来也是安全的。

  ★三种死法

  说完了两套API,开始来讨论一下线程的几种死法。线程和进程一样,也有三种死法。详见如下:

  1、自然死亡

  一般来说,每个线程都会对应某个函数(以下称为“线程函数”)。线程函数是线程运行的主体。所谓的“自然死亡”,就是通过return 语句结束线程函数的执行。

  2、自杀

  所谓的“自杀”,就是当前线程通过调用某API把自己 给停掉。前面已经说了OS API的坏话,同学们应该明白不能 再用它们。那我们能否使用CRT API来进行自杀呢?请看MSDN上的相关文档 。上面说了,如果使用_endthread 和_endthreadex ,将导致析构函数不被 调用。

  3、它杀

  所谓的“它杀”,很明显,就是其它线程通过调用某API把当前线程给强行 停掉。对于Windows平台来说,实现“它杀”比较简单,使用TernimateThread 就直接干掉了(它杀也是最野蛮的)。

  ★类对象的析构

  把类对象分为三种:局部非静态对象、局部静态对象、非局部对象。由于非局部对象是在main之前就创建、在进程死亡时析构,暂时与线程扯不上太大关系。剩下的两种局部对象,在宿主线程(所谓宿主线程,就是创建该局部对象的线程)死亡时会受到什么影响捏?请看如下的对照表:

  -------------------------

  局部非静态对象  局部静态对象

  自然死亡    能        能

  自杀     不能       能

  它杀     不能       能

  -------------------------

  从上述结果可以看出,Windows上线程的死法还是以自然死亡为最安全,这点和进程的死法类似。所以同学们在Windows上开发时,要尽量避免自杀和它杀。

  ★关于主线程之死

  所谓“主线程”,就是进程启动时,操作系统为该进程默认创建的第一个线程。通俗地讲,可以把main 函数看成是主线程的线程函数。

  主线程之死是有讲究的。由于前面已经阐述了非自然死亡的坏处,所以我们只讨论主线程自然死亡这一种情况。当主线程自然死亡时(也就是用return 从main 返回时),会导致exit 函数被调用,exit 函数就会开始清除当前进程的各种资源,为进程的死亡作准备。这时候,如果还有其它活着的线程,也会被一起干掉(其效果类似于它杀)。

  为了防止出现上述情况,主线程一定要负责最终的善后工作。务必等到其它线程都死了,它才能死。

原创粉丝点击