若要使多线程C和C + +程序能够正确地运行,必须创建一个数据结构,并将它与使用
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 t的C / 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那样,返回新创建
132计计第二部分编程的具体方法
下载
的线程的句柄。因此,如果调用源代码中的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 t为C / 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中。代换重新打印它的源代码,这里提供了它的伪代码版
本,并且列出它的一些令人感兴趣的要点:
第6章线程的基础知识计计133
下载
下面是关于_ 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。
清单6-1 C/C++运行期库的线程局部t i d d a t a结构
134计计第二部分编程的具体方法
下载
既然为新线程指定了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文件中)。这里是该函数的伪代码版本:
第6章线程的基础知识计计135
下载
下面是关于_ 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文件中)。下面是
该函数的伪代码版本:
136计计第二部分编程的具体方法
下载
下面是关于_ 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 t的Visual 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的值,那么上面的代码将不进行编译。
第6章线程的基础知识计计137
下载
多线程版本的C / C + +运行期库还给某些函数设置了同步的基本要素。例如,如果两个线程
同时调用m a l l o c,那么内存堆栈就可能遭到破坏。多线程版本的C / C + +运行期库能够防止两个
线程同时从堆栈中分配内存。为此,它要让第二个线程等待,直到第一个线程从m a l l o c返回。
然后第二个线程才被允许进入(关于线程同步的问题将在第8、9章和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中的错误,应用程序中的错误也会自动得到解决。