多线程编程技术开发资料
来源:互联网 发布:全知之眼是什么梗 编辑:程序博客网 时间:2024/05/15 18:48
多线程编程技术开发资料
目录
Win32 多线程的性能(1)... 1
Win32 多线程的性能(2)... 10
关于多线程的一些细节... 23
用VC++5.0 实 现 多 线 程 的 调 度 和 处 理... 25
一 多 任 务, 多 进 程 和 多 线 程... 25
二 基 于MFC 的 多 线 程 编 程... 26
三 编 程 实 例... 29
用VC++5实现多线程... 35
Windows95下多线程编程技术及其实现... 40
多线程编程应注意的问题... 44
多线程程序设计... 45
Visual C++ 5.0中的多线程编程技术... 50
关于线程... 62
采用多线程进行数据采集... 64
循环创建多线程时保证参数的有效性... 67
MFC中多线程的应用... 70
线程通信初探... 84
VC++多线程下内存操作的优化... 88
任务,过程,和线程... 95
使用临界段实现优化的进程间同步对象-原理和实现... 100
Win32 多线程的性能(1)
作者:Microsoft公司供稿
Ruediger R. Asche
Microsoft Developer Network 技术小组
摘要
本文讨论将单线程应用程序重新编写成多线程应用程序的策略。它以Microsoft? Windows? 95和Windows NT?的平台为例,从吞吐量(throughput)和响应方面,与兼容的单线程计算相比较而分析了多线程计算的性能。
介绍
在您所能够找到的有关多线程的资料中,多数都是讲述同步概念的。例如,如何串行化(serialize)共享公共数据的线程。这种把重点放在讨论同步上是有意义的,因为同步是多线程编程中不可缺少的一部分。本文则后退了一步(takes a step back),主要讨论有关多线程中很少有人涉及的一面:决定一个计算如何能够被有意义地拆分为多个线程。本文中所使用的示例程序,THRDPERF,在Microsoft? Windows? 95和Windows NT? 两个平台之上,针对同一个计算采取串行和并发两种方法分别实现了测试套件(test suite),并从吞吐量和性能两方面来比较它们。
本文的第一部分建立了一些有关多线程应用程序的词汇(vocabulary),讨论测试套件的范围,并且介绍了示例程序套件是如何设计的。第二部分讨论测试的结果,并且包括对于多线程应用程序设计的建议。与之相关的文章 "Interacting with Microsoft Excel: A Case Study in OLE Automation" 讨论有关该示例程序套件的一个有趣的问题,即使用测试集合所获得的数据是如何使用 OLE Automation被输入 Microsoft Excel 中的。
如果您是经验丰富的多线程应用程序编程者,您可以跳过介绍部分,而直接到下面的“结果”部分。
多线程词汇
很长一段时间以来,您的应用程序一直被使用——它运转出色,是可以信赖的,而且 the whole bit——但它十分的迟缓,并且您有如何利用多线程的想法。但是,在开始这样做之前请稍等一会儿,因为这里有许多的陷阱,它们使您相信某种多线程设计是非常完美的,但实际上并不是这样。
在您跳至有关要进入的结论之前,首先让我们澄清一下在本文中将不讨论的内容:
在 Microsoft Win32? 应用程序编程接口(API)下提供多线程访问的库是不同的,但是我们不关注这一问题。示例程序套件,Threadlib.exe,是在一个Microsoft Foundation Class Library (MFC)应用程序中使用Win32多线程API来编写的,但是,您是使用Microsoft C运行时(CRT)库、MFC库,还是单纯的(barebones) Win32 API来创建和维持线程,我们并不关心。
实际上,每一种库最后都要调用 Win32 系统服务CreateThread来创建一个工作线程,并且多线程本身总是要通过操作系统来执行。您想要使用哪一种包装机制将不会影响本文的论题。当然,您是使用某一个还是使用其它的包装库(wrapper library),可能会引起性能上的差异,但是在这儿,我们主要讨论多线程的本质,而不关心其包装(wrapper)。
本文所讨论的是在单处理器机器上运行的多线程应用程序。多处理器计算机则是一个完全不同的主题,并且本文中所讨论的结论,几乎没有一个可以应用于多处理器的机器中。我还没有这样的机会在一个运行 Windows NT 系统的可调整的(scalable)对称多线程(SMP)机器上执行该示例。如果您有这样的机会,我非常高兴地希望知道您的结果。
在本文中,我更喜欢一般性地引用“计算”。计算被定义为您的应用程序的一个子任务,可以被作为整体或部分来执行,可以早于或迟于另一个计算,或者与其他的计算同时发生。例如,让我们假设某个应用程序需要用户的数据,并且需要保存这些数据到磁盘。我们可以假定输入数据包含一种计算,而保存这些数据则是另一种计算。根据应用程序的计算的设计,下面两种情况都是可能的:一种是数据的保存和新数据的输入是同时交叉进行的;另一种是直到用户已经输入了全部的数据才可是将数据保存到磁盘上。第一种情况一般可以使用某种形式的多线程来实现;我们称这种组织计算的方式为并发或交互。后一种情况一般可以用单线程应用程序来实现,在本文中,它被称为串行执行。
有关并发应用程序的设计是一个非常复杂的过程。一般非常有钱的(who make a ton of money)人比较喜欢它,因为要计算出一个给定的任务采用并发执行到底有多大的好处,通常需要多年的研究。本文并不想要教您如何设计多线程应用程序。相反,我要向您指出某些多线程应用程序设计的问题所在,而且,我使用真实(real-life)的性能测试来讨论我的例子。在阅读过本文后,您应该能够观察一个给定的设计,并且能够决定某种设计是否提高了该应用程序的整体性能。
多线程应用程序设计步骤中的一部分工作,就是要决定在何处存在可能潜在地引起数据毁坏的多线程数据访问冲突,以及如何使用线程的同步来避免这种冲突。这项任务(以后,本文将称之为线程编序(thread serialization))是许多有关多线程的文章的主题,(例如,MSDN Library中的 "Synchronization on the Fly"或"Compound Win32 Synchronization Objects"),在本文中将丝毫不涉及对它的讨论。有关在本文中要讨论的,我们将假定需要并发的计算并不共享任何数据,并且因此而不需要任何线程编序。这种约定看起来可能有点苛刻,但是请您牢记,不可能有关于同步多线程应用程序的“通用”的讨论,因为每一次编序都将强加一个唯一的“等待-醒来”结构(waiting-and-waking pattern)到已编序的线程,它将直接地影响性能。
Win32下的大多数输入/输出(I/O)操作有两种形态:同步或异步。已经被证明在许多的情况下,一个使用同步I/O的多线程设计可以被使用异步单线程I/O的设计来模拟。本文并不讨论作为多线程替代形式的异步单线程I/O,但是,我建议您最好两种设计都考虑。
注意到Win32 I/O系统设计的方式是提供一些机制,使得异步I/O要优于同步I/O(例如,I/O全能端口(completion ports))。我计划在以后的文章中讨论有关同步I/O和异步I/O的问题。
正如在"Multiple Threads in the User Interface"一文中所指出的,多线程和图形用户界面(GUI)不能很好地共同工作。在本文中,我假设后台线程可以执行其工作而根本不需要使用Windows GUI;我所处理的这种类型的线程仅仅是“工作线程”,它仅在后台执行计算,而不需要与用户的直接交互。
有有限计算,同样也有与之相对应的无限计算。服务器端应用程序中的一个“倾听”线程就是无限计算的一个例子,它没有任何的目的,只是等待一个客户连接到服务器。在一个客户已经连接之后,该线程就发送一个通知到主线程,并且返回到“倾听”状态,直到下一个客户的连接。很自然,这样的一种计算不可能驻留在同一个作为应用程序用户界面(UI)的线程之中,除非使用一种异步I/O操作。(请注意,这个特定的问题能够,也应该通过使用异步I/O和全能(completion)端口来解决,而不是使用多线程,我在这里使用这个例子仅仅是用作演示)。在本文中,我将只考虑有限计算,就是说,应用程序的子任务将在有限的时间段之后结束。
基于CPU的计算和基于I/O的计算
对于一个单个的线程,决定所给定的计算是否是一个优秀的方案的最重要因素是,该计算是一个基于CPU的计算还是基于I/O的计算。基于CPU的计算是指这种计算的大多数时间CPU都非常“忙”。典型的基于CPU的计算如下:
复杂的数学计算,例如复数的计算、图形的处理、或屏幕后台图形计算
对驻留在内存中的文件图像的操作,例如在一个文本文件的内存镜像中的给定字符串。
相比较而言,基于I/O的计算是这样的一种计算,它的大多数时间要花费在等待I/O请求的结束。在大多数的操作系统中,正在进入的设备I/O将被异步地处理,可能是由一个专门的I/O处理器来处理,或由一个有效率的中断处理程序来处理,并且,来自于某个应用程序的I/O请求将会挂起调用线程,直到I/O结束。一般来说,花费大部分时间来等待I/O请求的线程不会与其他的线程争夺CPU时间;因此,同基于CPU的线程相比,基于I/O的计算可能不会降低其他线程的性能,(稍后,我将解释这一论点)
但是请注意,这种比较是非常理论性的。大多数的计算都不是纯粹的基于I/O的或纯粹的基于CPU的,而是基于I/O的计算和基于CPU的计算都包含。同一集合的计算可能在一种方案中使用顺序计算而运行良好,而在另一种方案中使用并发的计算,这取决于基于CPU的计算和基于I/O的计算的相对划分。
多线程设计的目标
在想要对您的应用程序应用多线程之前,您应该问问自己这种转变的目标是什么。多线程有许多潜在的优点:
增强的性能
增强的容量(throughput)
更好地用户快速响应(responsiveness)
让我们依次讨论上面的每一个优点。
性能
考虑到时间,让我们简单地定义“性能”就是给定的一个或一组计算所消耗的全部时间。按照其定义,则性能的比较就仅仅是对有限计算而言的。
无论您相信与否,多线程方案对应用程序的性能的提高是非常有限的。这里面的原因不是很明显,但是它非常有道理:
除非是该应用程序运行于一个多处理器的机器上,(在这种情况下,子计算真正地是并行执行的),基于CPU的计算在多线程情况下不可能比在单线程情况下的执行速度快。这是因为,无论计算被分解成小块(在多线程的情况下)或大块(在同一线程中计算按顺序挨个执行的情况下),只有一个CPU,而且它必需执行所有的计算。结果是,对于一组给定的计算,如果是以多个线程来执行,那么一般会比按串行方式计算完成的时间要长,因为它增加了创建线程和在线程之间切换CPU的额外负担。
一般来说,必定会有某些情况,无论多个计算的完成谁先谁后,但是它们的结果必需同步。例如,使用多个线程来并发的读多个文件到内存中,那么文件被处理的顺序我们是不关心的,但是必需等到所有的数据都读入内存之后,应用程序才能开始处理。我们将在“容量”一节讨论这个想法。
在本文中,我们将以消耗的时间,即完成所有的计算所消耗的总的时间,来衡量性能。
容量(Throughput)
容量(或响应),是指每一个计算的平均处理周期(turnaround)的时间。为了演示容量,让我们假设一个超级市场的例子(它总是一个有关操作系统的极好的演示工具):假设每一个计算就是一个在结算柜台被服务的顾客。对于超级市场来说,既可以为每一个顾客开设一个结算柜台,也可以把所有的顾客集中起来通过一个结算柜台。为了我们分析的需要,假设是有多个结算柜台的情况,但是,仅有一个收银员(可怜的家伙!)来服务所有的顾客,而不考虑顾客是在一个柜台前排队或多个柜台前排队。这个超级收银员将高速地从一个柜台跳到下一个柜台,一次仅处理(ringing up)一个顾客的一件商品,然后,就移动到下一个顾客。这个超级的收银员就象是被多个计算所割裂的CPU。
就象我们在前面的“性能”一节中所看到的,服务所有顾客的总的时间并没有因为有多个结算柜台打开而减少,因为无论顾客是在一个柜台还是多个柜台被服务,总是这一个收银员来完成所有的工作。但是,事情是这样,同只有一个结算柜台相比,顾客还是喜欢这种超级收银员的方式。这是因为一般情况下,顾客的手推车里的商品数的差别是巨大的,某些顾客的手推车中有一大堆的商品,而某些顾客则只想买很少几件商品。如果您曾经只希望买一盒 granola bars和一夸脱牛奶,而却排在某个来为全家24口人采购的先生后面,那您就知道我说的是意味着什么了。
无论怎样,如果您能够被 Clark Kent 先生以高速度服务,而不是在那里排队,您就不会太在意完成结帐的时间是否稍长,因为不管怎么样,两件商品都会很快地被处理完。而满载着为24口人采购的商品的手推车是在另一个柜台被处理的,所以您可以很快就完成结帐而离开。
因此,容量就是度量在一个给定的时间内有多少个计算可以被执行。每一个计算是这样度量它的进程的,那就是要比较以下的两个时间:完成本计算花费了多少的时间,以及假设该计算被首先处理的话要花费多少时间。换句话说,如果您去了超级市场,并且希望两分钟就离开那里,但是实际上您花费了两个小时来为您的两件商品结算,原因是您排在了购买其1997生产线的 Betty Crocker 的后面,那么不得不说,您的进程非常失败。
在本文中,我们这样定义一个计算的响应时间,计算完成所消耗的时间除以预计要消耗的时间。那么,如果一个应该消耗 10 毫秒(ms)的计算,而实际上消耗了 20 ms,那么它的响应处理周期就是 2,但是,如果就是同一个计算,却消耗了 200 ms (可能是因为有另一个长的计算与之竞争并优先)才结束,那么响应处理周期就是 20。显然,响应处理周期是越短越好。
我们在后面将会看到,在将多线程引入一个应用程序中时,即使导致了整体性能的下降,容量仍然可能是一个有实际意义的因素;但是,要使容量成为一个有实际意义的因素,必需满足下面的一些条件:
每一个计算必需是相互独立的,只要计算一结束,任何计算的结果都可以被处理或使用。如果您是某个大学足球队的队员,而且您们每一个队员都在同一个超级市场买自己的旅行食品,那么您的商品是先被处理还是后被处理、您花费了多长的时间为两件商品结帐、以及您为此等待了多长的时间,这些都无关紧要,因为最后您的汽车是不会离开的,除非所有的队员都买完了食品。所不同的只是您的等待时间,要么是花费在排队等待结帐,要么是如果超级收银员已经为您服务,时间就花费在等待其他人上。
这一点很重要,但却常被忽略。就象我前面所提到的,大多数的应用程序迟早都会显式或隐式地同步其计算。例如,如果您的应用程序从不同的文件并发地收集数据,您可能会想要在屏幕上显示结果,或者把它们保存到另一个文件中。在前面一种情况下(在屏幕上显示结果),您应该意识到,大多数图形系统都执行某种的内部批处理或串行操作,除非所有的输出数据都已收集到,否则是根本不会有好的显示的;在后面的情况下,(保存结果到另一个文件),除非整个原型文件已被写入完毕,一般不是您的应用程序(或其他的应用程序)所能完全处理的。所以,如果某个人或某些东西以某种形式将结果顺序化了,不管是应用程序、操作系统、甚至是用户,那么您在处理文件时所能得到的好处可能就会消失了。
计算之间在量上必需有明显的差异。如果超级市场中的每一个顾客都只有两件商品需要结帐,则超级收银员方式一点优势都没有;如果他不得不在3个结算柜台之间跳来跳去,而每一个要被服务的顾客仅有2个(或3个、4个或n个)商品要结算,那么每一个顾客都不得不等待几倍的时间来完成他或她的结算,这比让所有的顾客在一起排队还要糟糕。在这里把多线程想象为shock吸收装置:短的计算并不会冒被排在长的计算之后的危险,但是它们被分成线程并且花费了更多的时间,而本来它们可以在更短的时间内完成。
如果计算的长短可以事先决定,那么串行处理可能比多线程处理要好,您只要简单地以时间长短按升序排列计算就可以了。在超级市场的例子中,就相当于按照顾客的商品数来站排(Express Lane 方案的一种变种),这种想法是基于这样的考虑,只有很少的商品的顾客很喜欢它,因为他们不会为一点的事情而耽误很多的时间, 而那些有很多货物的顾客也不会在意,因为无论如何要完成其所有的结算都要花费很长的时间,而且在他们前面的每一个人的商品都比它少。
如果只是大致知道计算时间的一个范围,但是您的应用程序不能排序这些计算,那么您应该花些时间做一次最坏情况的分析。在这样的分析中,您应该假定这些计算不是按照时间的升序顺序来排序的,相反,它们是按照时间的降序来排序的。从响应这个角度来讲,这中方案是最坏的情形,因为按照前面所定义的公式,每一个计算都将具有其最高可能的响应处理周期。
快速响应(Responsiveness)
我将在这里讨论的、应用程序多线程化的最后一个准则是快速响应(在语言上与响应非常接近,足以使您迷惑不解)。在本文中,如果一个应用程序的设计是保证用户总是能够在一个很短的时间(很短的时间指时间非常短,使得用户感觉不到应用程序被挂起)内完成与应用程序的交互,那么我们就简单一点,定义该应用程序为响应快速的应用程序。
对于一个带有 GUI 的 Win32 应用程序,快速响应可以被很简单地实现,只要确保长的计算被委托给后台线程,但是实现快速响应所要求的结构可能要求较高的技巧,正如我前面所提到的,某些人可能会等待某个计算在某个时间返回,所以在后台执行一个长的计算可能需要改变用户界面(例如,需要添加一个“取消”按钮,并且依赖该计算结果的菜单项也不得不变灰)。
除了性能、容量和快速响应之外,其他的一些原因也可能影响多线程设计。例如,在某些结构下,必需让计算以一种伪随机方式(脑海中再次出现的例子是Bolzmann 机器类型的神经网络,在这种网络中,仅当该网络中的每一个节点异步执行其计算时,该互联网络的预期行为才能够工作)。但是,在本文中,我将把讨论的范围限制在上面所涉及的三个因素,那就是:性能、容量和快速响应。
测试的实现
我曾经听说过许多关于抽象(abstraction)机制的讨论,说它封装了所有多线程的糟糕(nasty)方面到一个 C++ 对象中,并且因此使一个应用程序获得了多线程的全部优点,而不是缺点。
在本文中,我一开始就设计这样一个抽象。我将为一个 C++ 的类 ConcurrentExecution 定义一个原型,该类将含有成员函数例如:DoConcurrent 和 DoSerial,并且这两个成员函数的参数将是一个普通对象数组和一个回调函数数组,这些回调函数将被每一个对象并发或串行地调用。该 C++ 类将封装所有关于保持该线程和内部数据结构的真实(gory)细节。
但是,对我来说,从一开始我就十分清楚,这样的一个抽象的用处十分有限,因为在设计一个多线程应用程序时的最大量的工作成了一个无法自动完成的任务,这个工作就是决定如何实现多线程。ConcurrentExecution 的第一个限制是回调函数将不允许显式或隐式的共享数据;或回调函数需要任何其他形式的同步操作,而这些同步操作将立刻牺牲掉所有该抽象所带来的优点,并且打开所有“精彩”的同步世界中的陷阱和圈套,例如死锁、竞争冲突、或需要非常复杂的复合同步对象。
同样,也不允许那些可能潜在地被并发执行的计算来调用 UI,因为就象我前面所讲到的,Win32 API 对于调用 UI 的线程强迫了许多个隐式的同步操作。请注意,还有许多其他的 API 子集和库对于共享它们的线程强迫了隐式的同步操作。
这些的限制使 ConcurrentExecution 只具有极其有限的功能,说具体一点,就是一个管理纯粹工作者线程的抽象(完全独立的计算大多数情况下仅限于在非连续内存区域的数学计算)。
然而,事实证明实现 ConcurrentExecution 类并且在性能测试中使用它是非常有用的,因为,当我实现了该类,并且设计和运行了该测试之时,许多关于多线程的隐藏起来的细节都暴露出来了。请清楚以下一点,虽然 ConcurrentExecution 类可以使多线程更容易处理,但是如果想要在商业产品中使用它,那么该类的实现还需要一些其他的工作。特别要提到的一点时,我忽略了所有的错误情况处理,这是不可忍受的。但是我假定只用于测试时(我明显地使用了 ConcurrentExecution),错误不会出现。
ConcurrentExecution 类
下面是 ConcurrentExecution 类的原型:
class ConcurrentExecution
{
< private members omitted>
public:
ConcurrentExecution(int iMaxNumberOfThreads);
~ConcurrentExecution();
int DoForAllObjects(int iNoOfObjects,long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,
CONCURRENT_FINISHING_ROUTINE pObjectTerminated);
BOOL DoSerial(int iNoOfObjects, long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,
CONCURRENT_FINISHING_ROUTINE pObjectTerminated);
};
该类是从 Thrdlib.dll 库中导出的,而 Thrdlib.dll 库是示例测试套件 THRDPERF 中的一个工程。在讨论该类的内部结构之前,让我们首先讨论成员函数的语义(semantics):
ConcurrentExecution::ConcurrentExecution(int iMaxNumberOfThreads)
{
m_iMaxArraySize = min(iMaxNumberOfThreads, MAXIMUM_WAIT_OBJECTS);
m_hThreadArray = (HANDLE *)VirtualAlloc(NULL,m_iMaxArraySize*sizeof(HANDLE),
MEM_COMMIT,PAGE_READWRITE);
m_hObjectArray = (DWORD *)VirtualAlloc(NULL,m_iMaxArraySize*sizeof(DWORD),
MEM_COMMIT,PAGE_READWRITE);
// 当然,一个真正的实现必需在这里提供对错误的处理...
};
您可能会注意到构造函数 ConcurrentExecution 有一个数字参数。该参数指定了该类的实例所支持的“并发的最大度数”;换句话说,如果某个 ConcurrentExecution 的实例被创建时,n 是它的一个参数,那么在任何给定的时间不能有超过 n 个计算在执行。根据我们以前的分析,该参数就意味“无论有多少个顾客在等待,打开的结算柜台数不要多于 n 个”。
int DoForAllObjects(int iNoOfObjects,long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,
CONCURRENT_FINISHING_ROUTINE pObjectTerminated);
这是在这里被实现的唯一有趣的成员函数。DoForAllObjects 的主要参数是一个对象的数组、一个处理器函数、和一个终结器函数。关于对象完全没有强制的格式;每次该处理器被调用时,将有一个对象被传递给它,而且完全由该处理器来解释对象。第一个参数 iNoOfObjects,仅仅是要 ConcurrentExecution 知道在对象数组中的元素数。请注意,在调用 DoForAllObjects 时,如果对象数组的长度为 1,那么它与调用 CreateThread 就非常相似(有一点不同,那就是 CreateThread 不接受一个终结器参数)。
DoForAllObjects 的语义如下:处理器将为每一个对象而调用。对象被处理的顺序并未指定;所有能够担保的只是每一个对象都将在某个时间被传递给处理器。并发的最大度数是由传递给 ConcurrentExecution 对象的构造函数的参数来决定的。
处理器函数不能访问共享的数据,并且不能调用到 UI 或做任何其他需要显式或隐式地串行操作的事情。目前,仅存在一个处理器函数能够对所有的对象工作;但是,要使用处理器数组来替代该处理器参数将是简单的。
该处理器的原型如下:
typedef DWORD (WINAPI *CONCURRENT_EXECUTION_ROUTINE)
(LPVOID lpParameterBlock);
当该处理器已经完成了在一个对象上的工作之后,终结器函数将立即被调用。与处理器不同,终结器函数是在该调用函数的环境中被串行调用的,并且可以调用所有的例程和访问调用程序所能够访问的所有数据。但是,应该要注意的是,终结器应该被尽可能地优化,因为终结器中的长计算会影响 DoForAllObjects 的性能。请注意,尽管只要处理器结束了每一个对象终结器就会立即被调用,直到最后一个对象已经被终结之前,DoForAllObjects 本身并没有返回。
我们为什么要经历这么多使用终结器的痛苦?我们同样可以让每一个计算在处理器函数的最终结束时执行终结器代码,是吗?
这样基本上是可以的;但是,有必要强调终结器是在调用 DoForAllObjects的线程环境中被调用的。这样的设计使在每一个计算进入时处理它们的结果更加容易,而无须担心同步问题。
终结器函数的原型如下:
typedef DWORD (WINAPI *CONCURRENT_FINISHING_ROUTINE)
(LPVOID lpParameterBlock,LPVOID lpResultCode);
第一个参数是被处理的对象,第二个参数是处理器函数在该对象上的结果。
DoForAllObjects 的同类是 DoSerial,DoSerial 与 DoForAllObjects 具有相同的参数列表,但是计算是被以串行的顺序处理的,并且以列表中的第一个对象开始。
Win32 多线程的性能(2)
作者:Microsoft公司供稿
Ruediger R. Asche
Microsoft Developer Network 技术小组
ConcurrentExecution 的内部工作
请注意:本节的讨论是非常技术性的,所以假设您理解很多有关 Win32 线程 API 的知识。如果您对如何使用 ConcurrentExecution 类来收集测试数据更加感兴趣,而不是对 ConcurrentExecution::DoForAllObjects 是如何被实现的感兴趣,那么您现在就可以跳到下面的“使用 ConcurrentExecution 来试验线程性能”一节。
让我们从 DoSerial 开始,因为它很大程度上是一个“不费脑筋的家伙”:
BOOL ConcurrentExecution::DoSerial(int iNoOfObjects,long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pProcessor,
CONCURRENT_FINISHING_ROUTINE pTerminator)
{
for (int iLoop=0;iLoop<iNoOfObjects;iLoop++)
{
pTerminator((LPVOID)ObjectArray[iLoop],(LPVOID)pProcessor((LPVOID)ObjectArray[iLoop]));
};
return TRUE;
};
这段代码只是循环遍历该数组,在每一次迭代中调用处理器,然后在处理器和对象本身的结果上调用终结器。干得既干净又漂亮,不是吗?
令人感兴趣的成员函数是 DoForAllObjects。乍一看,DoForAllObjects 所要做的也没有什么特别的——请求操作系统创建为每一个计算一个线程,并且确保终结器函数能够被正确地调用。但是,有两个问题使得 DoForAllObjects 比它的表面现象要复杂:第一,当计算的数目多于可用的线程数时,ConcurrentExecution 的一个实例所创建的“并发的最大度数”参数可能需要一些附加的记录(bookkeeping)。第二,每一个计算的终结器函数都是在调用 DoForAllObjects 的线程的上下文中被调用的,而不是在该计算运行所处的线程上下文中被调用的;并且,终结器是在处理器结束之后立刻被调用的。要处理这些问题还是需要很多技巧的。
让我们深入到代码中,看看究竟是怎么样的。该段代码是从文件 Thrdlib.cpp 中继承来的,但是为了清除起见,已经被精简了:
int ConcurrentExecution::DoForAllObjects(int iNoOfObjects,long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,
CONCURRENT_FINISHING_ROUTINE
pObjectTerminated)
{
int iLoop,iEndLoop;
DWORD iThread;
DWORD iArrayIndex;
DWORD dwReturnCode;
DWORD iCurrentArrayLength=0;
BOOL bWeFreedSomething;
char szBuf[70];
m_iCurrentNumberOfThreads=iNoOfObjects;
HANDLE *hPnt=(HANDLE *)VirtualAlloc(NULL,m_iCurrentNumberOfThreads*sizeof(HANDLE)
,MEM_COMMIT,PAGE_READWRITE);
for(iLoop=0;iLoop<m_iCurrentNumberOfThreads;iLoop++)
hPnt[iLoop] = CreateThread(NULL,0,pObjectProcessor,(LPVOID)ObjectArray[iLoop],
CREATE_SUSPENDED,(LPDWORD)&iThread);
首先,我们为每一个对象创建单独的线程。因为我们使用 CREATE_SUSPENDED 来创建该线程,所以还没有线程被启动。另一种方法是在需要时创建每一个线程。我决定不使用这种替代的策略,因为我发现当在一个同时运行了多个线程的应用程序中调用时, CreateThread 调用是非常浪费的;这样,同在运行时创建每一个线程相比,在此时创建线程的开销将更加容易接受,
for (iLoop = 0; iLoop < m_iCurrentNumberOfThreads; iLoop++)
{
HANDLE hNewThread;
bWeFreedSomething=FALSE;
// 如果数组为空,分配一个 slot 和 boogie。
if (!iCurrentArrayLength)
{
iArrayIndex = 0;
iCurrentArrayLength=1;
}
else
{
// 首先,检查我们是否可以重复使用任何的 slot。我们希望在查找一个新的 slot 之前首先// 做这项工作,这样我们就可以立刻调用该就线程的终结器...
iArrayIndex=WaitForMultipleObjects(iCurrentArrayLength,
m_hThreadArray,FALSE,0);
if (iArrayIndex==WAIT_TIMEOUT) // no slot free...
{
{
if (iCurrentArrayLength >= m_iMaxArraySize)
{
iArrayIndex= WaitForMultipleObjects(iCurrentArrayLength,
m_hThreadArray,FALSE,INFINITE);
bWeFreedSomething=TRUE;
}
else // 我们可以释放某处的一个 slot,现在就这么做...
{
iCurrentArrayLength++;
iArrayIndex=iCurrentArrayLength-1;
}; // Else iArrayIndex points to a thread that has been nuked
};
}
else bWeFreedSomething = TRUE;
}; // 在这里,iArrayIndex 包含一个有效的索引以存储新的线程。
hNewThread = hPnt[iLoop];
ResumeThread(hNewThread);
if (bWeFreedSomething)
{
GetExitCodeThread(m_hThreadArray[iArrayIndex],&dwReturnCode); //错误
CloseHandle(m_hThreadArray[iArrayIndex]);
pObjectTerminated((void *)m_hObjectArray[iArrayIndex],(void *)dwReturnCode);
};
m_hThreadArray[iArrayIndex] = hNewThread;
m_hObjectArray[iArrayIndex] = ObjectArray[iLoop];
}; // 循环结束
DoForAllObjects 的核心是 hPnt,它是一个对象数组,这些对象是当 ConcurrentExecution 对象被构造时分配的。该数组能够容纳最大数目的线程,此最大数目与在构造函数中指定的最大并发度数相对应;因此,该数组中的每一个元素都是一个"slot",并有一个计算居于之中。
关于决定如何填充和释放的 slots 算法如下:该对象数组是从头到尾遍历的,并且对于每一个对象,我们都做如下的事情:如果尚未有 slot 已经被填充,我们使用当前的对象来填充该数组中的第一个 slot,并且继续执行将要处理当前对象的线程。如果至少有一个 slot 被使用,我们使用 WaitForMultipleObjects 函数来决定是否有正在运行的任何计算已经结束;如果是,我们在该对象上调用终结器,并且为新对象“重用”该 slot。请注意,我们也可以首先填充每一个空闲的 slot,直到没有剩余的 slots 为止,然后开始填充空的 slot。但是,如果我们这样做了,那么空出 slot 的终结器函数将不会被调用,直到所有的 slot 都已经被填充,这样就违反了我们有关当处理器结束一个对象时,终结器立刻被调用的要求。
最后,还有没有空闲 slot 的情况(就是说,当前激活的线程数等于 ConcurrentExecution 对象所允许的最大并发度数)。在这种情况下,WaitForMultipleObjects 将被再次调用以使得 DoForAllObjects 处于“睡眠”状态,直到有一个 slot 空出;只要这种情况一发生,终结器就被在空出 slot 的对象上调用,并且工作于当前对象的线程被继续执行。
终于,所有的计算要么都已经结束,要么将占有对象数组中的 slot。下列的代码将会处理所有剩余的线程:
iEndLoop = iCurrentArrayLength;
for (iLoop=iEndLoop;iLoop>0;iLoop--)
{
iArrayIndex=WaitForMultipleObjects(iLoop, m_hThreadArray,FALSE,INFINITE);
if (iArrayIndex==WAIT_FAILED)
{
GetLastError();
_asm int 3; // 这里要做一些聪明的事...
};
GetExitCodeThread(m_hThreadArray[iArrayIndex],&dwReturnCode); // 错误?
if (!CloseHandle(m_hThreadArray[iArrayIndex]))
MessageBox(GetFocus(),"Can't delete thread!","",MB_OK); // 使它更好...
pObjectTerminated((void *)m_hObjectArray[iArrayIndex],
(void *)dwReturnCode);
if (iArrayIndex==iLoop-1) continue; // 这里很好,没有需要向后填充
m_hThreadArray[iArrayIndex]=m_hThreadArray[iLoop-1];
m_hObjectArray[iArrayIndex]=m_hObjectArray[iLoop-1];
};
最后,清除:
if (hPnt) VirtualFree(hPnt,m_iCurrentNumberOfThreads*sizeof(HANDLE),
MEM_DECOMMIT);
return iCurrentArrayLength;
};
使用 ConcurrentExecution 来试验线程性能
性能测试的范围如下:测试应用程序 Threadlibtest.exe 的用户可以指定是否测试基于 CPU 的或基于 I/O 的计算、执行多少个计算、计算的时间有多长、计算是如何排序的(为了测试最糟的情况与随机延迟),以及计算是被并发执行还是串行执行。
为了消除意外的结果,每一个测试可以被执行十次,然后将十次的结果拿来平均,以产生一个更加可信的结果。
通过选择菜单选项 "Run entire test set",用户可以请求运行所有测试变量的变形。在测试中使用的计算长度在基础值 10 和 3,500 ms 之间变动(我一会儿将讨论这一问题),计算的数目在 2 和 20 之间变化。如果在运行该测试的计算机上安装了 Microsoft Excel,Threadlibtest.exe 将会把结果转储在一个 Microsoft Excel 工作表,该工作表位于 C:/Temp/Values.xls。在任何情况下结果值也将会被保存到一个纯文本文件中,该文件位于 C:/Temp/Results.fil。请注意,我对于协议文件的位置使用了硬编码的方式,纯粹是懒惰行为;如果您需要在您的计算机上重新生成测试结果,并且需要指定一个不同的位置,那么只需要重新编译生成该工程,改变文件 Threadlibtestview.cpp 的开头部分的 TEXTFILELOC 和 SHEETFILELOC 标识符的值即可。
请牢记,运行整个的测试程序将总是以最糟的情况来排序计算(就是说,执行的顺序是串行的,最长的计算将被首先执行,其后跟随着第二长的计算,然后以次类推)。这种方案牺牲了串行执行的灵活性,因为并发执行的响应时间在一个非最糟的方案下也没有改变,而该串行执行的响应时间是有可能提高的。
正如我前面所提到的,在一个实际的方案中,您应该分析每一个计算的时间是否是可以预测的。
使用 ConcurrentExecution 类来收集性能数据的代码位于 Threadlibtestview.cpp 中。示例应用程序本身 (Threadlibtest.exe) 是一个真正的单文档界面 (SDI) 的 MFC 应用程序。所有与示例有关的代码都驻留在 view 类的实现 CThreadLibTestView 中,它是从 CEasyOutputView 继承而来的。(有关对该类的讨论,请参考"Windows NT Security in Theory and Practice"。)这里并不包含该类中所有的有趣代码,所包含的大部分是其数字统计部分和用户界面处理部分。执行测试中的 "meat" 在 CThreadLibTestView::ExecuteTest 中,将执行一个测试运行周期。下面是有关 CThreadLibTestView::ExecuteTest 的简略代码:
void CThreadlibtestView::ExecuteTest()
{
ConcurrentExecution *ce;
bCPUBound=((m_iCompType&CT_IOBOUND)==0); // 全局...
ce = new ConcurrentExecution(25);
if (!QueryPerformanceCounter(&m_liOldVal)) return; // 获得当前时间。
if (!m_iCompType&CT_IOBOUND) timeBeginPeriod(1);
if (m_iCompType&CT_CONCURRENT)
m_iThreadsUsed=ce->DoForAllObjects(m_iNumberOfThreads,
(long *)m_iNumbers,
(CONCURRENT_EXECUTION_ROUTINE)pProcessor,
(CONCURRENT_FINISHING_ROUTINE)pTerminator);
else
ce->DoSerial(m_iNumberOfThreads,
(long *)m_iNumbers,
(CONCURRENT_EXECUTION_ROUTINE)pProcessor,
(CONCURRENT_FINISHING_ROUTINE)pTerminator);
if (!m_iCompType&CT_IOBOUND) timeEndPeriod(1);
delete(ce);
< 其他的代码在一个数组中排序结果,以供 Excel 处理...>
}
该段代码首先创建一个 ConcurrentExecution 类的对象,然后,取样当前时间,(用于统计计算所消耗的时间和响应时间),并且,根据所请求的是串行执行还是并发执行,分别调用 ConcurrentExecution 对象 DoSerial 或 DoForAllObjects 成员。请注意,对于当前的执行我请求最大并发度数为 25;如果您想要运行有多于 25 个计算的测试程序,那么您应该提高该值,使它大于或等于运行您的测试程序所需要的最大并发数。
让我们看一下处理器和终结器,以得到精确测量的结果:
extern "C"
{
long WINAPI pProcessor(long iArg)
{
PTHREADBLOCKSTRUCT ptArg=(PTHREADBLOCKSTRUCT)iArg;
BOOL bResult=TRUE;
int iDelay=(ptArg->iDelay);
if (bCPUBound)
{
int iLoopCount;
iLoopCount=(int)(((float)iDelay/1000.0)*ptArg->tbOutputTarget->m_iBiasFactor);
QueryPerformanceCounter(&ptArg->liStart);
for (int iCounter=0; iCounter<iLoopCount; iCounter++);
}
else
{
QueryPerformanceCounter(&ptArg->liStart);
Sleep(ptArg->iDelay);
};
return bResult;
}
long WINAPI pTerminator(long iArg, long iReturnCode)
{
PTHREADBLOCKSTRUCT ptArg=(PTHREADBLOCKSTRUCT)iArg;
QueryPerformanceCounter(&ptArg->liFinish);
ptArg->iEndOrder=iEndIndex++;
return(0);
}
}
处理器模拟一个计算,其长度已经被放到一个与计算有关的数据结构 THREADBLOCKSTRUCT 中。THREADBLOCKSTRUCT 保持着与计算有关的数据,如其延迟和终止时间(以性能计数“滴答”来衡量),以及反向指针,指向实用化该结构的视图(view)。
通过简单的使计算“睡眠”指定的时间就可以模拟基于I/O的计算。基于 CPU的计算将进入一个空的 for 循环。这里的一些注释是为了帮助理解代码的功能:计算是基于 CPU 的,并且假定其执行时间为指定的毫秒数。在本测试程序的早期版本中,我仅仅是要 for 循环执行足够多的次数以满足指定的延迟的需求,而不考虑数字的实际含义。(根据相关的代码,对于基于I/O的计算该数字实际意味着毫秒,而对于基于CPU的计算,该数字则意味着迭代次数。)但是,为了能够使用绝对时间来比较基于CPU的计算和基于I/O的计算,我决定重写这段代码,这样无论对于基于CPU的计算还是基于I/O的计算,与计算有关的延迟都是以毫秒测量。
我发现对于具有指定的、预先定义时间长度的基于CPU的计算,要编写代码来模拟它并不是一件简单的事情。原因是这样的代码本身不能查询系统时间,因为所引发的调用迟早都会交出 CPU,而这违背了基于 CPU 的计算的要求。试图使用异步多媒体时钟事件同样没有得到满意的效果,原因是 Windows NT 下计时器服务的工作方式。设置了一个多媒体计时器的线程实际上被挂起,直到该计时器回调被调用;因此,基于 CPU 的计算突然变成了基于 I/O 的操作了。
于是,最后我使用了一个有点儿低劣的技巧:CThreadLibTestView::OnCreate 中的代码执行 100 次循环从 1 到 100,000 计数,并且取样通过该循环所需要的平均时间。结果被保存在成员变量 m_iBiasFactor 中,该成员变量是一个浮点数,它在处理器函数中被使用来决定毫秒如何被“翻译”成迭代次数。不幸的是,因为操作系统的高度戏剧性的天性,要决定实际上运行一个指定长度的计算要迭代多少次给定的循环是困难的。但是,我发现该策略在决定基于CPU的操作的计算时间方面,完成了非常可信的工作。
注意 如果您重新编译生成该测试应用程序,请小心使用最优化选项。如果您指定了 "Minimize execution time" 优化,则编译程序将检测具有空的主体的 for 循环,并且删除这些循环。
终结器非常简单:当前时间被取样并保存在计算的 THREADBLOCKSTRUCT 中。在测试结束之后,该代码计算执行 ExecuteTest 的时间和终结器为每一个计算所调用的时间之间的差异。然后,所有计算所消耗的时间由所有已完成的计算中最后一个计算完成时所消耗的时间所决定,而响应时间则是每一个计算的响应时间的平均值,这里,每一个响应时间,同样,定义为从测试开始该线程消耗的时间除以该线程的延迟因子。请注意,终结器在主线程上下文中串行化的运行,所以在共享的 iEndIndex 变量上的递增指令是安全的。
这些实际就是本测试的全部;其余的部分则主要是为测试的运行设置一些参数,以及对结果执行一些数学计算。填充结果到 Microsoft Excel 工作单中的相关逻辑将在"Interacting with Microsoft Excel: A Case Study in OLE Automation."一文中讨论。
结果
如果您想要在您的计算机上重新创建该测试结果,您需要做以下的事情:
如果您需要改变测试参数,例如最大计算数或协议文件的位置,请编辑 THRDPERF 示例工程中的 Threadlibtestview.cpp,然后重新编译生成该应用程序。(请注意,要编译生成该应用程序,您的计算机需要对长文件名的支持。)
请确保文件 Thrdlib.dll 在一个 Threadlibtest.exe 能够链接到它的位置。
如果您想要使用 Microsoft Excel 来查看测试的结果,请确定 Microsoft Excel 已经正确地被安装在运行该测试的计算机上。
从 Windows 95 或 Windows NT 执行 Threadlibtest.exe,并且从“Run performance tests”菜单选择"Run entire test set"。正常情况下,测试运行一次要花费几个小时才能完成。
在测试结束之后,检查结果时,既可以使用普通文本协议文件 C:/Temp/Results.fil ,也可以使用工作单文件 C:/Temp/Values.xls。请注意,Microsoft Excel 的自动化(automation)逻辑并不自动地为您从原始数据生成图表,我使用了几个宏来重新排列该结果,并且为您生成了图表。我憎恨数字(number crunching),但是我必需称赞 Microsoft Excel,因为即使象我一样的工作表妄想狂(spreadsheet-paranoid),也可以提供这样漂亮的用户界面,在几分钟之内把几列数据装入有用的图表。
我所展现的测试结果是在一个 486/33 MHz 的带有 16 MB RAM 的系统收集而来的。该计算机同时安装了 Windows NT (3.51 版) 和 Windows 95;这样,在两个操作系统上的不同测试结果就是可比的了,因为它们基于同样的硬件系统。
那么,现在让我们来解释这些值。这里是总结计算结果的图表;后面有其解释。该图表应该这样来看:每一个图表的 x 轴有 6 个值(除了有关长计算的消耗时间表,该表仅含有 5 个值,这是因为在我的测试运行时,对于非常长的计算计时器溢出了)。一个值代表多个计算;我以 2、5、8、11、14 和 17 个计算来运行每一个测试。在 Microsoft Excel 结果工作表中,您将会找到对于基于CPU的计算和基于I/O的计算的线程的每一种计算数目的结果,延迟(delay bias)分别是 10 ms、30 ms、90 ms、270 ms,、810 ms 和 2430 ms,但是在该图表中,我仅包括了 10 ms 和 2430 ms 的结果,这样所有的数字都被简化,而且更容易理解。
我需要解释 "delay bias." 的含义,如果一个测试运行的 delay bias 是 n,则每一个计算都有一个倍数 n 作为其计算时间。例如,如果试验的是 delay bias 为 10 的 5 个计算,则其中一个计算将执行 50 ms,第二个将执行 40 ms,第三个将执行 30 ms,第四个将执行 20 ms,而第五个将执行 10 ms。并且,当这些计算被串行执行时,假定为最糟的情况,所以具有最长延迟的计算首先被执行,其他的计算按降序排列其后。于是,在“理想”情况下(就是说,计算之间没有重叠),对于基于CPU的计算来说,全部所需的时间将是 50 ms + 40 ms + 30 ms + 20 ms + 10 ms = 150 ms。
对于消耗时间图表来说, y 轴的值与毫秒对应,对于响应时间图表来说,y 轴的值与相对(relative turnaround)长度(即,实际执行所花费的毫秒数除以预期的毫秒数)相对应。
图 1. 短计算消耗时间比较,在 Windows NT 下
图 2. 长计算消耗时间比较,在 Windows NT 下
图 3. 短计算响应时间比较,在 Windows NT 下
图 4. 长计算响应时间比较,在 Windows NT 下
图 5. 短计算消耗时间比较,在 Windows 95 下
图 6. 长计算消耗时间比较,在 Windows 95 下
图 7. 短计算响应时间比较,在 Windows 95 下
图 8. 长计算响应时间比较,在 Windows 95 下
基于 I/O 的任务
以消耗时间和 turnaround 时间来衡量,基于 I/O 的线程当并发执行时比串行执行要好得多。作为计算得一个功能,对于并发执行来说,消耗时间以线性模式递增,而对于串行执行来说,则以指数模式递增(有关 Windows NT 请参阅图 1 和 2,有关 Windows 95 请参阅图 5 和 6)。
请注意,这个结论与我们前面对基于 I/O 的计算的分析是一致的,基于 I/O 的计算是多线程的优秀候选人,因为一个线程在等待 I/O 请求结束时被挂起,而这段时间该线程不会占用 CPU 时间,于是,这段时间就可以被其他的线程所使用。
对于并发计算来说,平均响应时间是一个常数,对于串行计算来说,平均响应时间则线性递增(请分别参阅图 3、4、7 和 8)。
请注意无论任何情况,只有少数几个计算执行的方案中,无论串行或并发的执行,无论测试参数如何设置,并没有什么明显的区别。
基于 CPU 的任务
正如我们前面所提到的,在一个单处理器的计算机中,基于 CPU 的任务的并发执行速度不可能比串行执行速度快,但是我们可以看到,在 Windows NT 下线程创建和切换的额外开销非常小;对于非常短的计算,并发执行仅仅比串行执行慢 10%,而随着计算长度的增加,这两个时间就非常接近了。以响应时间来衡量,我们可以发现对于长计算,并发执行相对于串行执行的响应增益可以达到 50%,但是对于短的计算,串行执行实际上比并发执行更加好。
Windows 95 和 Windows NT 之间的比较
如果我们看一看有关长计算的图表(即,图2、4、6 和 8),我们可以发现在 Windows 95 和 Windows NT 下其行为是极其类似的。请不要被这样的事实所迷惑,即好象 Windows 95 处理基于I/O的计算与基于CPU的计算不同于 Windows NT。我把这一结果的原因归结为这样一个事实,那就是我用来决定多少个测试循环与 1 毫秒相对应的算法(如前面所述)是非常不精确的;我发现同样是这个算法,在完全相同的环境下执行多次时,所产生的结果之间的差异最大时可达20%。所以,比较基于 CPU 的计算和基于 I/O 的计算实际上是不公平的。
Windows 95 和 Windows NT 之间不同的一点是当针对短的计算时。如我们从图1 和5 所看到的,对于并发的基于I/O的短计算,Windows NT 的效果要好得多。我把这一结果得原因归结为更加有效率得线程创建方案。请注意,对于长得计算,串行与并发I/O操作之间的差别消失了,所以这里我们处理的是固定的、相对比较小的额外开销。
对于短的计算,以响应时间来衡量(如图 3 和 7),请注意,在 Windows NT 下,在10个线程处有一个断点,在这里更多的计算并发执行有更好的效果,而对于 Windows 95 ,则是串行计算有更好的容量。
请注意这些比较都是基于当前版本的操作系统得到的(Windows NT 3.51 版和 Windows 95),如果考虑到操作系统的问题,那么线程引擎非常有可能被增强,所以两个操作系统之间的各自行为的差异有可能消失。但是,有一点很有趣的要注意,短计算一般不必要使用多线程,尤其是在 Windows 95 下。
建议
这些结果可以推出下面的建议:决定多线程性能的最主要因素是基于 I/O 的计算和基于 CPU 的计算的比例,决定是否采用多线程的主要条件是前台的用户响应。
让我们假定在您的应用程序中,有多个子计算可以在不同的线程中潜在地被执行。为了决定对这些计算使用多线程是否有意义,要考虑下面的几点。
如果用户界面响应分析决定某些事情应该在第二线程中实现,那么,决定将要执行的任务是基于I/O的计算还是基于CPU 的计算就很有意义。基于I/O的计算最好被重新定位到后台线程中。(但是,请注意,异步单线程的 I/O 处理可能比多线程同步I/O要好,这要看问题而定)非常长的基于CPU的线程可能从在不同的线程中被执行获益;但是,除非该线程的响应非常重要,否则,在同一个后台线程中执行所有的基于 CPU 的任务可能比在不同的线程中执行它更有意义。请记住在任何的情况下,短计算在并发执行时一般会在线程创建时有非常大的额外开销。
如果对于基于CPU的计算 — 即每一个计算的结果只要得到了就立刻能应用的计算,响应是最关键的,那么,您应该尝试决定这些计算是否能够以升序排序,在此种情况下这些计算串行执行的整体性能仍然会比并行执行要好。请注意,有一些计算机的体系结构的设计是为了能够非常有效率地处理长的计算(例如矩阵操作),那么,在这样的计算机上对长的计算实行多线程化的话,您可能实际上牺牲了这种结构的优势。
所有的这些分析都假定该应用程序是运行在一个单处理器的计算机上,并且计算之间是相互独立的。实际上,如果计算之间相互依赖而需要串行设计,串行执行的性能将不受影响(因为串行是隐式的),而并发执行的版本将总是受到不利的影响。
我还建议您基于计算之间相互依赖的程度决定多线程的设计。在大多数情况下子计算线程化不用说是好的,但是,如果对于拆分您的应用程序为多个可以在不同线程处理的子计算的方法有多种选择,我推荐您使用同步的复杂性作为一个条件。换句话说,如果拆分成多个线程而需要非常少和非常直接的同步,那么这种方案就比需要使用大量且复杂的线程同步的方案要好。
最后一个请注意是,请牢记线程是一种系统资源,不是无代价的;所以,there may be a greater penalty to multithreading than performance hits alone. 作为第一规则(rule of thumb),我建议您在使用多线程时要保持理智并且谨慎。在多线程确实能够给您的应用程序设计带来好处的时候才使用它们,但是,如果串行计算可以达到同样的效果,就不要使用它们。
总结
运行附加的性能测试套件产生了一些特殊的结果,这些结果提供了许多有关并发应用程序设计的内部逻辑。请注意我的许多假定是非常基本的;我选择了比较非常长的计算和非常短的计算,我假定计算之间完全独立,要么完全是基于I/O的计算,要么是完全基于CPU的计算。而绝大多数的现实问题,如果以计算长度和 boundedness 来衡量,都是介于这两种情况之间的。请把本文中的材料仅仅作为一个起点,它使您更加详细地分析您的应用程序,以决定是否使用多线程。
在本系列的将来的一篇文章中,我将讨论通过异步I/O来增强基于I/O的操作的性能。
关于多线程的一些细节
作者: coolnerd
线程的程序中,如果线程要向界面窗口报告状态,有两种操作方法,
一种是通过消息的方法,由于消息本身携带的消息量有时不购用,往往消息参数
只是一个指向某消息对象的指针,而消息对象往往需要在堆内存中new生成,
(因为往往线程不能等待消息处理完毕就继续执行,所以如果消息对象是栈对象
往往消息对象还未来及被处理,就又被线程修改.所以采用堆对象.)
界面接受到
消息对象后delete之.但是这时界面退出后,如果线程仍然生成新的消息对象,
则消息对象得不到释放,所以在这种情况下,界面接受到WM_CLOSE消息将要释放
之前,要等待线程完全退出之后再真正释放.
线程向界面报告状态的第二种方法是直接在线程的执行过程中同步地(等待,
直到完成称为同步)执行界面显示,这种机制下,要注意在界面显示是需要查看
界面窗口是否仍然存在(使用IsWindow(hWnd)函数实现).
这样做似乎已经完美,但是是不完善的,因为假如有多个view小窗口,如多个
CSplitterWnd,只在一个CSplitterWnd的WM_CLOSE消息的处理函数中进行防范,
其他的CSplitterWnd照常退出,仍然要出问题,所以要抓住根源:
用户使用菜单退出或点击frmae窗口的x按钮退出,接受到退出消息的首先是frameWnd
所以需要在frameWnd的WM_CLOSE函数中进行线程的释放.!
另外,往往线程在Document类的掌管之下,frame怎样访问document对象?
FrameWnd没有直接提供获取document的函数.Document,View,FrameWnd三者的
创建顺序是:doc->Frmae->View,在View::InitUpdate()函数的执行时刻,
可以执行以下代码:
CMainFrame*frm=(CMainFrame*)(AfxGetApp()->m_pMainWnd);
frm->pDoc=GetDocument();
另外:注意需要捕捉WM_CLOSE,而非WM_DESTROY消息,因为WM_CLOSE消息
先于后者.
另外,考查以下代码:
void CThreadList::UpdateThread(int id,CString client,CString msg)
{
EnterCriticalSection(&CThreadList::csUpDateThread);
{
int ItemCount=m_ListCtrl.GetItemCount() ; //ListCtrl
最多65535条记录
if(id>ItemCount)
{
for(int i=0;i<id-ItemCount;i++)
{
LV_ITEM lvi;
lvi.mask = LVIF_TEXT | LVIF_IMAGE
/* |LVIF_STATE */;
lvi.iItem = ItemCount+i;
lvi.iSubItem = 0;
m_ListCtrl.InsertItem(&lvi);
//m_ListCtrl.SetItemCount(id);
}
}
m_ListCtrl.SetItemText(id-1,0,ito10a(id));
m_ListCtrl.SetItemText(id-1,1,client);
m_ListCtrl.SetItemText(id-1,2,msg);
}
LeaveCriticalSection(&CThreadList::csUpDateThread);
}
这就是线程用来调用的界面函数,该界面CThreadList
是个ListCtrl类,该成员函数的参数中,id,client,msg是界面显示的内容
函数首先判断id是否超出现在已经存在的个数,如果超出则增加一到多条记录
这种动态调整记录个数的机制比较诱人,但是如果这个函数是在这样的情况下
被调用: frame窗口接受到了WM_CLOSE消息,在处理消息之前,首先消灭线程
而就在消灭线程的过程中,一个未来及消灭的线程调用了这个函数,该函数在
执行过程中需要执行GetItemCount()函数,跟踪GetItemCount函数,发现它是
依靠执行SendMessage获得ItemCount的(SendMessage函数是个不等到结果不
返回的函数),这时会发生的是死机,因为Windows系统在处理WM_ClOSE消息
未完成时,又被要求处理SendMessage函数,于是SendMessage函数和WM_CLOSE
消息处理过程发生了互相等待的事故.
结论是不要在线程向界面报告状态的过程中调用任何依靠消息工作的函数.
经过考查,几乎所有更新界面控件的函数如SetItemText都是依靠SendMessage
来工作的,所以会到问题的最初:"在多线程的程序中,如果线程要向界面窗口
报告状态,有两种操作方法"在这两种方法中,第二种方法是行不通的.
实用技巧
用VC++5.0 实 现 多 线 程 的 调 度 和 处 理
一 多 任 务, 多 进 程 和 多 线 程
---- Windows95 和WindowsNT 操 作 系 统 支 持 多 任 务 调 度 和 处 理, 基 于 该 功 能 所 提 供 的 多 任 务 空 间, 程 序 员 可 以 完 全 控 制 应 用 程 序 中 每 一 个 片 段 的 运 行, 从 而 编 写 高 效 率 的 应 用 程 序。
---- 所 谓 多 任 务 通 常 包 括 这 样 两 大 类: 多 进 程 和 多 线 程。 进 程 是 指 在 系 统 中 正 在 运 行 的 一 个 应 用 程 序; 线 程 是 系 统 分 配 处 理 器 时 间 资 源 的 基 本 单 元, 或 者 说 进 程 之 内 独 立 执 行 的 一 个 单 元。 对 于 操 作 系 统 而 言, 其 调 度 单 元 是 线 程。 一 个 进 程 至 少 包 括 一 个 线 程, 通 常 将 该 线 程 称 为 主 线 程。 一 个 进 程 从 主 线 程 的 执 行 开 始 进 而 创 建 一 个 或 多 个 附 加 线 程, 就 是 所 谓 基 于 多 线 程 的 多 任 务。
---- 开 发 多 线 程 应 用 程 序 可 以 利 用32 位Windows 环 境 提 供 的Win32 API 接 口 函 数, 也 可 以 利 用VC++ 中 提 供 的MFC 类 库 进 行 开 发。 多 线 程 编 程 在 这 两 种 方 式 下 原 理 是 一 样 的, 用 户 可 以 根 据 需 要 选 择 相 应 的 工 具。 本 文 重 点 讲 述 用VC++5.0 提 供 的MFC 类 库 实 现 多 线 程 调 度 与 处 理 的 方 法 以 及 由 线 程 多 任 务 所 引 发 的 同 步 多 任 务 特 征, 最 后 详 细 解 释 一 个 实 现 多 线 程 的 例 程。
二 基 于MFC 的 多 线 程 编 程
---- 1 MFC 对 多 线 程 的 支 持
---- MFC 类 库 提 供 了 多 线 程 编 程 支 持, 对 于 用 户 编 程 实 现 来 说 更 加 方 便。 非 常 重 要 的 一 点 就 是, 在 多 窗 口 线 程 情 况 下,MFC 直 接 提 供 了 用 户 接 口 线 程 的 设 计。
---- MFC 区 分 两 种 类 型 的 线 程: 辅 助 线 程(Worker Thread) 和 用 户 界 面 线 程(UserInterface Thread)。 辅 助 线 程 没 有 消 息 机 制, 通 常 用 来 执 行 后 台 计 算 和 维 护 任 务。MFC 为 用 户 界 面 线 程 提 供 消 息 机 制, 用 来 处 理 用 户 的 输 入, 响 应 用 户 产 生 的 事 件 和 消 息。 但 对 于Win32 的API 来 说, 这 两 种 线 程 并 没 有 区 别, 它 只 需 要 线 程 的 启 动 地 址 以 便 启 动 线 程 执 行 任 务。 用 户 界 面 线 程 的 一 个 典 型 应 用 就 是 类CWinApp, 大 家 对 类CwinApp 都 比 较 熟 悉, 它 是CWinThread 类 的 派 生 类, 应 用 程 序 的 主 线 程 是 由 它 提 供, 并 由 它 负 责 处 理 用 户 产 生 的 事 件 和 消 息。 类CwinThread 是 用 户 接 口 线 程 的 基 本 类。CWinThread 的 对 象 用 以 维 护 特 定 线 程 的 局 部 数 据。 因 为 处 理 线 程 局 部 数 据 依 赖 于 类CWinThread, 所 以 所 有 使 用MFC 的 线 程 都 必 须 由MFC 来 创 建。 例 如, 由run-time 函 数_beginthreadex 创 建 的 线 程 就 不 能 使 用 任 何MFC API。
---- 2 辅 助 线 程 和 用 户 界 面 线 程 的 创 建 和 终 止
---- 要 创 建 一 个 线 程, 需 要 调 用 函 数AfxBeginThread。 该 函 数 通 过 参 数 重 载 具 有 两 种 版 本, 分 别 对 应 辅 助 线 程 和 用 户 界 面 线 程。 无 论 是 辅 助 线 程 还 是 用 户 界 面 线 程, 都 需 要 指 定 额 外 的 参 数 以 修 改 优 先 级, 堆 栈 大 小, 创 建 标 志 和 安 全 特 性 等。 函 数AfxBeginThread 返 回 指 向CWinThread 类 对 象 的 指 针。
---- 创 建 助 手 线 程 相 对 简 单。 只 需 要 两 步: 实 现 控 制 函 数 和 启 动 线 程。 它 并 不 必 须 从CWinThread 派 生 一 个 类。 简 要 说 明 如 下:
---- 1. 实 现 控 制 函 数。 控 制 函 数 定 义 该 线 程。 当 进 入 该 函 数, 线 程 启 动; 退 出 时, 线 程 终 止。 该 控 制 函 数 声 明 如 下:
UINT MyControllingFunction( LPVOID pParam );
---- 该 参 数 是 一 个 单 精 度32 位 值。 该 参 数 接 收 的 值 将 在 线 程 对 象 创 建 时 传 递 给 构 造 函 数。 控 制 函 数 将 用 某 种 方 式 解 释 该 值。 可 以 是 数 量 值, 或 是 指 向 包 括 多 个 参 数 的 结 构 的 指 针, 甚 至 可 以 被 忽 略。 如 果 该 参 数 是 指 结 构, 则 不 仅 可 以 将 数 据 从 调 用 函 数 传 给 线 程, 也 可 以 从 线 程 回 传 给 调 用 函 数。 如 果 使 用 这 样 的 结 构 回 传 数 据, 当 结 果 准 备 好 的 时 候, 线 程 要 通 知 调 用 函 数。 当 函 数 结 束 时, 应 返 回 一 个UINT 类 型 的 值 值, 指 明 结 束 的 原 因。 通 常, 返 回0 表 明 成 功, 其 它 值 分 别 代 表 不 同 的 错 误。
---- 2. 启 动 线 程。 由 函 数AfxBeginThread 创 建 并 初 始 化 一 个CWinThread 类 的 对 象, 启 动 并 返 回 该 线 程 的 地 址。 则 线 程 进 入 运 行 状 态。
---- 3. 举 例 说 明。 下 面 用 简 单 的 代 码 说 明 怎 样 定 义 一 个 控 制 函 数 以 及 如 何 在 程 序 的 其 它 部 分 使 用。
UINT MyThreadProc( LPVOID pParam )
{
CMyObject* pObject = (CMyObject*)pParam;
if (pObject == NULL ||
!pObject- >IsKindOf(RUNTIME_CLASS(CMyObject)))
return -1; //非法参数
……//具体实现内容
return 0; //线程成功结束
}
//在程序中调用线程的函数
……
pNewObject = new CMyObject;
AfxBeginThread(MyThreadProc, pNewObject);
……
创建用户界面线程有两种方法。
---- 第 一 种 方 法, 首 先 从CWinTread 类 派 生 一 个 类( 注 意 必 须 要 用 宏DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 对 该 类 进 行 声 明 和 实 现); 然 后 调 用 函 数AfxBeginThread 创 建CWinThread 派 生 类 的 对 象 进 行 初 始 化 启 动 线 程 运 行。 除 了 调 用 函 数AfxBeginThread 之 外, 也 可 以 采 用 第 二 种 方 法, 即 先 通 过 构 造 函 数 创 建 类CWinThread 的 一 个 对 象, 然 后 由 程 序 员 调 用 函 数::CreateThread 来 启 动 线 程。 通 常 类CWinThread 的 对 象 在 该 线 程 的 生 存 期 结 束 时 将 自 动 终 止, 如 果 程 序 员 希 望 自 己 来 控 制, 则 需 要 将m_bAutoDelete 设 为FALSE。 这 样 在 线 程 终 止 之 后 类CWinThread 对 象 仍 然 存 在, 只 是 在 这 种 情 况 下 需 要 手 动 删 除CWinThread 对 象。
---- 通 常 线 程 函 数 结 束 之 后, 线 程 将 自 行 终 止。 类CwinThread 将 为 我 们 完 成 结 束 线 程 的 工 作。 如 果 在 线 程 的 执 行 过 程 中 程 序 员 希 望 强 行 终 止 线 程 的 话, 则 需 要 在 线 程 内 部 调 用AfxEndThread(nExitCode)。 其 参 数 为 线 程 结 束 码。 这 样 将 终 止 线 程 的 运 行, 并 释 放 线 程 所 占 用 的 资 源。 如 果 从 另 一 个 线 程 来 终 止 该 线 程, 则 必 须 在 两 个 线 程 之 间 设 置 通 信 方 法。 如 果 从 线 程 外 部 来 终 止 线 程 的 话, 还 可 以 使 用Win32 函 数(CWinThread 类 不 提 供 该 成 员 函 数):BOOL TerminateThread(HANDLE hThread,DWORD dwExitcode)。 但 在 实 际 程 序 设 计 中 对 该 函 数 的 使 用 一 定 要 谨 慎, 因 为 一 旦 该 命 令 发 出, 将 立 即 终 止 该 线 程, 并 不 释 放 线 程 所 占 用 的 资 源, 这 样 可 能 会 引 起 系 统 不 稳 定。
---- 如 果 所 终 止 的 线 程 是 进 程 内 的 最 后 一 个 线 程, 则 在 该 线 程 终 止 之 后 进 程 也 相 应 终 止。
---- 3 进 程 和 线 程 的 优 先 级 问 题
---- 在Windows95 和WindowsNT 操 作 系 统 当 中, 任 务 是 有 优 先 级 的, 共 有32 级, 从0 到31, 系 统 按 照 不 同 的 优 先 级 调 度 线 程 的 运 行。
---- 1) 0-15 级 是 普 通 优 先 级, 线 程 的 优 先 级 可 以 动 态 变 化。 高 优 先 级 线 程 优 先 运 行, 只 有 高 优 先 级 线 程 不 运 行 时, 才 调 度 低 优 先 级 线 程 运 行。 优 先 级 相 同 的 线 程 按 照 时 间 片 轮 流 运 行。 2) 16-30 级 是 实 时 优 先 级, 实 时 优 先 级 与 普 通 优 先 级 的 最 大 区 别 在 于 相 同 优 先 级 进 程 的 运 行 不 按 照 时 间 片 轮 转, 而 是 先 运 行 的 线 程 就 先 控 制CPU, 如 果 它 不 主 动 放 弃 控 制, 同 级 或 低 优 先 级 的 线 程 就 无 法 运 行。
---- 一 个 线 程 的 优 先 级 首 先 属 于 一 个 类, 然 后 是 其 在 该 类 中 的 相 对 位 置。 线 程 优 先 级 的 计 算 可 以 如 下 式 表 示:
---- 线 程 优 先 级= 进 程 类 基 本 优 先 级+ 线 程 相 对 优 先 级
---- 进 程 类 的 基 本 优 先 级:
IDLE_PROCESS_CLASS
NORMAL_PROCESS_CLASS
HIGH_PROCESS_CLASS
REAL_TIME_PROCESS_CLASS
线程的相对优先级:
THREAD_PRIORITY_IDLE
(最低优先级,仅在系统空闲时执行)
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_NORMAL (缺省)
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_CRITICAL
(非常高的优先级)
---- 4 线 程 同 步 问 题
---- 编 写 多 线 程 应 用 程 序 的 最 重 要 的 问 题 就 是 线 程 之 间 的 资 源 同 步 访 问。 因 为 多 个 线 程 在 共 享 资 源 时 如 果 发 生 访 问 冲 突 通 常 会 产 生 不 正 确 的 结 果。 例 如, 一 个 线 程 正 在 更 新 一 个 结 构 的 内 容 的 同 时 另 一 个 线 程 正 试 图 读 取 同 一 个 结 构。 结 果, 我 们 将 无 法 得 知 所 读 取 的 数 据 是 什 么 状 态: 旧 数 据, 新 数 据, 还 是 二 者 的 混 合 ?
---- MFC 提 供 了 一 组 同 步 和 同 步 访 问 类 来 解 决 这 个 问 题, 包 括:
---- 同 步 对 象:CSyncObject, CSemaphore, CMutex, CcriticalSection 和CEvent ; 同 步 访 问 对 象:CMultiLock 和 CSingleLock 。
---- 同 步 类 用 于 当 访 问 资 源 时 保 证 资 源 的 整 体 性。 其 中CsyncObject 是 其 它 四 个 同 步 类 的 基 类, 不 直 接 使 用。 信 号 同 步 类CSemaphore 通 常 用 于 当 一 个 应 用 程 序 中 同 时 有 多 个 线 程 访 问 一 个 资 源( 例 如, 应 用 程 序 允 许 对 同 一 个Document 有 多 个View) 的 情 况; 事 件 同 步 类CEvent 通 常 用 于 在 应 用 程 序 访 问 资 源 之 前 应 用 程 序 必 须 等 待( 比 如, 在 数 据 写 进 一 个 文 件 之 前 数 据 必 须 从 通 信 端 口 得 到) 的 情 况; 而 对 于 互 斥 同 步 类CMutex 和 临 界 区 同 步 类CcriticalSection 都 是 用 于 保 证 一 个 资 源 一 次 只 能 有 一 个 线 程 访 问, 二 者 的 不 同 之 处 在 于 前 者 允 许 有 多 个 应 用 程 序 使 用 该 资 源( 例 如, 该 资 源 在 一 个DLL 当 中) 而 后 者 则 不 允 许 对 同 一 个 资 源 的 访 问 超 出 进 程 的 范 畴, 而 且 使 用 临 界 区 的 方 式 效 率 比 较 高。
---- 同 步 访 问 类 用 于 获 得 对 这 些 控 制 资 源 的 访 问。CMultiLock 和 CSingleLock 的 区 别 仅 在 于 是 需 要 控 制 访 问 多 个 还 是 单 个 资 源 对 象。
---- 5 同 步 类 的 使 用 方 法
---- 解 决 同 步 问 题 的 一 个 简 单 的 方 法 就 是 将 同 步 类 融 入 共 享 类 当 中, 通 常 我 们 把 这 样 的 共 享 类 称 为 线 程 安 全 类。 下 面 举 例 来 说 明 这 些 同 步 类 的 使 用 方 法。 比 如, 一 个 用 以 维 护 一 个 帐 户 的 连 接 列 表 的 应 用 程 序。 该 应 用 程 序 允 许3 个 帐 户 在 不 同 的 窗 口 中 检 测, 但 一 次 只 能 更 新 一 个 帐 户。 当 一 个 帐 户 更 新 之 后, 需 要 将 更 新 的 数 据 通 过 网 络 传 给 一 个 数 据 文 档。
---- 该 例 中 将 使 用3 种 同 步 类。 由 于 允 许 一 次 检 测3 个 帐 户, 使 用CSemaphore 来 限 制 对3 个 视 窗 对 象 的 访 问。 当 更 新 一 个 帐 目 时, 应 用 程 序 使 用CCriticalSection 来 保 证 一 次 只 有 一 个 帐 目 更 新。 在 更 新 成 功 之 后, 发CEvent 信 号, 该 信 号 释 放 一 个 等 待 接 收 信 号 事 件 的 线 程。 该 线 程 将 新 数 据 传 给 数 据 文 档。
---- 要 设 计 一 个 线 程 安 全 类, 首 先 根 据 具 体 情 况 在 类 中 加 入 同 步 类 做 为 数 据 成 员。 在 例 子 当 中, 可 以 将 一 个CSemaphore 类 的 数 据 成 员 加 入 视 窗 类 中, 一 个CCriticalSection 类 数 据 成 员 加 入 连 接 列 表 类, 而 一 个CEvent 数 据 成 员 加 入 数 据 存 储 类 中。
---- 然 后, 在 使 用 共 享 资 源 的 函 数 当 中, 将 同 步 类 与 同 步 访 问 类 的 一 个 锁 对 象 联 系 起 来。 即, 在 访 问 控 制 资 源 的 成 员 函 数 中 应 该 创 建 一 个CSingleLock 或 CMultiLock 的 对 象 并 调 用 该 对 象 的Lock 函 数。 当 访 问 结 束 之 后, 调 用UnLock 函 数, 释 放 资 源。
---- 用 这 种 方 式 来 设 计 线 程 安 全 类 比 较 容 易。 在 保 证 线 程 安 全 的 同 时, 省 去 了 维 护 同 步 代 码 的 麻 烦, 这 也 正 是OOP 的 思 想。 但 是 使 用 线 程 安 全 类 方 法 编 程 比 不 考 虑 线 程 安 全 要 复 杂, 尤 其 体 现 在 程 序 调 试 过 程 中。 而 且 线 程 安 全 编 程 还 会 损 失 一 部 分 效 率, 比 如 在 单CPU 计 算 机 中 多 个 线 程 之 间 的 切 换 会 占 用 一 部 分 资 源。
三 编 程 实 例
---- 下 面 以VC++5.0 中 一 个 简 单 的 基 于 对 话 框 的MFC 例 程 来 说 明 实 现 多 线 程 任 务 调 度 与 处 理 的 方 法, 下 面 加 以 详 细 解 释。
---- 在 该 例 程 当 中 定 义 两 个 用 户 界 面 线 程, 一 个 显 示 线 程(CDisplayThread) 和 一 个 计 数 线 程(CCounterThread)。 这 两 个 线 程 同 时 操 作 一 个 字 符 串 变 量m_strNumber, 其 中 显 示 线 程 将 该 字 符 串 在 一 个 列 表 框 中 显 示, 而 计 数 线 程 则 将 该 字 符 串 中 的 整 数 加1。 在 例 程 中, 可 以 分 别 调 整 进 程、 计 数 线 程 和 显 示 线 程 的 优 先 级。 例 程 中 的 同 步 机 制 使 用CMutex 和CSingleLock 来 保 证 两 个 线 程 不 能 同 时 访 问 该 字 符 串。 同 步 机 制 执 行 与 否 将 明 显 影 响 程 序 的 执 行 结 果。 在 该 例 程 中 允 许 将 将 把 两 个 线 程 暂 时 挂 起, 以 查 看 运 行 结 果。 例 程 中 还 允 许 查 看 计 数 线 程 的 运 行。 该 例 程 中 所 处 理 的 问 题 也 是 多 线 程 编 程 中 非 常 具 有 典 型 意 义 的 问 题。
---- 在 该 程 序 执 行 时 主 要 有 三 个 用 于 调 整 优 先 级 的 组 合 框, 三 个 分 别 用 于 选 择 同 步 机 制、 显 示 计 数 线 程 运 行 和 挂 起 线 程 的 复 选 框 以 及 一 个 用 于 显 示 运 行 结 果 的 列 表 框。
---- 在 本 程 序 中 使 用 了 两 个 线 程 类CCounterThread 和CDisplayThread, 这 两 个 线 程 类 共 同 操 作 定 义 在CMutexesDlg 中 的 字 符 串 对 象m_strNumber。 本 程 序 对 同 步 类CMutex 的 使 用 方 法 就 是 按 照 本 文 所 讲 述 的 融 入 的 方 法 来 实 现 的。 同 步 访 问 类CSingleLock 的 锁 对 象 则 在 各 线 程 的 具 体 实 现 中 定 义。
---- 下 面 介 绍 该 例 程 的 具 体 实 现:
1. 利 用AppWizard 生 成 一 个 名 为Mutexes 基 于 对 话 框 的 应 用 程 序 框 架。
2. 利 用 对 话 框 编 辑 器 在 对 话 框 中 填 加 以 下 内 容: 三 个 组 合 框, 三 个 复 选 框 和 一 个 列 表 框。 三 个 组 合 框 分 别 允 许 改 变 进 程 优 先 级 和 两 个 线 程 优 先 级, 其ID 分 别 设 置 为:IDC_PRIORITYCLASS、IDC_DSPYTHRDPRIORITY 和IDC_CNTRTHRDPRIORITY。 三 个 复 选 框 分 别 对 应 着 同 步 机 制 选 项、 显 示 计 数 线 程 执 行 选 项 和 暂 停 选 项, 其ID 分 别 设 置 为IDC_SYNCHRONIZE、IDC_SHOWCNTRTHRD 和IDC_PAUSE。 列 表 框 用 于 显 示 线 程 显 示 程 序 中 两 个 线 程 的 共 同 操 作 对 象m_strNumber, 其ID 设 置 为IDC_DATABOX。
3. 创 建 类CWinThread 的 派 生 类CExampleThread。 该 类 将 作 为 本 程 序 中 使 用 的 两 个 线 程 类:CCounterThread 和CDisplayThread 的 父 类。 这 样 做 的 目 的 仅 是 为 了 共 享 两 个 线 程 类 的 共 用 变 量 和 函 数。
---- 在CExampleThread 的 头 文 件 中 填 加 如 下 变 量:
CMutexesDlg * m_pOwner;//指向类CMutexesDlg指针
BOOL m_bDone;//用以控制线程执行
及函数:
void SetOwner(CMutexesDlg* pOwner)
{ m_pOwner=pOwner; };//取类CMutexesDlg的指针
然后在构造函数当中对成员变量进行初始化:
m_bDone=FALSE;//初始化允许线程运行
m_pOwner=NULL;//将该指针置为空
m_bAutoDelete=FALSE;//要求手动删除线程对象
4. 创 建 两 个 线 程 类CCounterThread 和CdisplayThread。 这 两 个 线 程 类 是CExampleThread 的 派 生 类。 分 别 重 载 两 个 线 程 函 数 中 的::Run() 函 数, 实 现 各 线 程 的 任 务。 在 这 两 个 类 当 中 分 别 加 入 同 步 访 问 类 的 锁 对 象sLock, 这 里 将 根 据 同 步 机 制 的 复 选 与 否 来 确 定 是 否 控 制 对 共 享 资 源 的 访 问。 不 要 忘 记 需 要 加 入 头 文 件#include "afxmt.h"。
---- 计 数 线 程::Run() 函 数 的 重 载 代 码 为:
int CCounterThread::Run()
{
BOOL fSyncChecked;//同步机制复选检测
unsigned int nNumber;//存储字符串中整数
if (m_pOwner == NULL)
return -1;
//将同步对象同锁对象联系起来
CSingleLock sLock(&(m_pOwner- >m_mutex));
while (!m_bDone)//控制线程运行,为终止线程服务
{
//取同步机制复选状态
fSyncChecked = m_pOwner- >
IsDlgButtonChecked(IDC_SYNCHRONIZE);
//确定是否使用同步机制
if (fSyncChecked)
sLock.Lock();
//读取整数
_stscanf((LPCTSTR) m_pOwner- >m_strNumber,
_T("%d"), &nNumber);
nNumber++;//加1
m_pOwner- >m_strNumber.Empty();//字符串置空
while (nNumber != 0) //更新字符串
{
m_pOwner- >m_strNumber +=
(TCHAR) ('0'+nNumber%10);
nNumber /= 10;
}
//调整字符串顺序
m_pOwner- >m_strNumber.MakeReverse();
//如果复选同步机制,释放资源
if (fSyncChecked)
sLock.Unlock();
//确定复选显示计数线程
if (m_pOwner- >IsDlgButtonChecked(IDC_SHOWCNTRTHRD))
m_pOwner- >AddToListBox(_T("Counter: Add 1"));
}//结束while
m_pOwner- >PostMessage(WM_CLOSE, 0, 0L);
return 0;
}
显示线程的::Run()函数重载代码为:
int CDisplayThread::Run()
{
BOOL fSyncChecked;
CString strBuffer;
ASSERT(m_pOwner != NULL);
if (m_pOwner == NULL)
return -1;
CSingleLock sLock(&(m_pOwner- >m_mutex));
while (!m_bDone)
{
fSyncChecked = m_pOwner- >
IsDlgButtonChecked(IDC_SYNCHRONIZE);
if (fSyncChecked)
sLock.Lock();
//构建要显示的字符串
strBuffer = _T("Display: ");
strBuffer += m_pOwner- >m_strNumber;
if (fSyncChecked)
sLock.Unlock();
//将字符串加入到列表框中
m_pOwner- >AddToListBox(strBuffer);
}//结束while
m_pOwner- >PostMessage(WM_CLOSE, 0, 0L);
return 0;
}
3在CMutexesDlg的头文件中加入如下成员变量:
CString m_strNumber;//线程所要操作的资源对象
CMutex m_mutex;//用于同步机制的互斥量
CCounterThread* m_pCounterThread;//指向计数线程的指针
CDisplayThread* m_pDisplayThread;//指向显示线程的指针
首先在对话框的初始化函数中加入如下代码对对话框进行初始化:
BOOL CMutexesDlg::OnInitDialog()
{
……
//初始化进程优先级组合框并置缺省为 NORMAL
CComboBox* pBox;
pBox = (CComboBox*) GetDlgItem(IDC_PRIORITYCLASS);
ASSERT(pBox != NULL);
if (pBox != NULL){
pBox- >AddString(_T("Idle"));
pBox- >AddString(_T("Normal"));
pBox- >AddString(_T("High"));
pBox- >AddString(_T("Realtime"));
pBox- >SetCurSel(1);
}
//初始化显示线程优先级组合框并置缺省为 NORMAL
pBox = (CComboBox*) GetDlgItem(IDC_DSPYTHRDPRIORITY);
ASSERT(pBox != NULL);
if (pBox != NULL){
pBox- >AddString(_T("Idle"));
pBox- >AddString(_T("Lowest"));
pBox- >AddString(_T("Below normal"));
pBox- >AddString(_T("Normal"));
pBox- >AddString(_T("Above normal"));
pBox- >AddString(_T("Highest"));
pBox- >AddString(_T("Timecritical"));
pBox- >SetCurSel(3);
}
//初始化计数线程优先级组合框并置缺省为 NORMAL
pBox = (CComboBox*) GetDlgItem(IDC_CNTRTHRDPRIORITY);
ASSERT(pBox != NULL);
if (pBox != NULL){
pBox- >AddString(_T("Idle"));
pBox- >AddString(_T("Lowest"));
pBox- >AddString(_T("Below normal"));
pBox- >AddString(_T("Normal"));
pBox- >AddString(_T("Above normal"));
pBox- >AddString(_T("Highest"));
pBox- >AddString(_T("Timecritical"));
pBox- >SetCurSel(3);
}
//初始化线程挂起复选框为挂起状态
CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE);
pCheck- >SetCheck(1);
//初始化线程
m_pDisplayThread = (CDisplayThread*)
AfxBeginThread(RUNTIME_CLASS(CDisplayThread),
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
m_pDisplayThread- >SetOwner(this);
m_pCounterThread = (CCounterThread*)
AfxBeginThread(RUNTIME_CLASS(CCounterThread),
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
m_pCounterThread- >SetOwner(this);
……
}
然后填加成员函数:
void AddToListBox(LPCTSTR szBuffer);//用于填加列表框显示
该函数的实现代码为:
void CMutexesDlg::AddToListBox(LPCTSTR szBuffer)
{
CListBox* pBox = (CListBox*) GetDlgItem(IDC_DATABOX);
ASSERT(pBox != NULL);
if (pBox != NULL){
int x = pBox- >AddString(szBuffer);
pBox- >SetCurSel(x);
if (pBox- >GetCount() > 100)
pBox- >DeleteString(0);
}
}
---- 然 后 利 用ClassWizard 填 加 用 于 调 整 进 程 优 先 级、 两 个 线 程 优 先 级 以 及 用 于 复 选 线 程 挂 起 的 函 数。
---- 调 整 进 程 优 先 级 的 代 码 为:
void CMutexesDlg::OnSelchangePriorityclass()
{
DWORD dw;
//取焦点选项
CComboBox* pBox = (CComboBox*)
GetDlgItem(IDC_PRIORITYCLASS);
int nCurSel = pBox- >GetCurSel();
switch (nCurSel)
{
case 0:
dw = IDLE_PRIORITY_CLASS;break;
case 1:
default:
dw = NORMAL_PRIORITY_CLASS;break;
case 2:
dw = HIGH_PRIORITY_CLASS;break;
case 3:
dw = REALTIME_PRIORITY_CLASS;break;
}
SetPriorityClass(GetCurrentProcess(), dw);//调整优先级
}
---- 由 于 调 整 两 个 线 程 优 先 级 的 代 码 基 本 相 似, 单 独 设 置 一 个 函 数 根 据 不 同 的ID 来 调 整 线 程 优 先 级。 该 函 数 代 码 为:
void CMutexesDlg::OnPriorityChange(UINT nID)
{
ASSERT(nID == IDC_CNTRTHRDPRIORITY ||
nID == IDC_DSPYTHRDPRIORITY);
DWORD dw;
//取对应该ID的焦点选项
CComboBox* pBox = (CComboBox*) GetDlgItem(nID);
int nCurSel = pBox- >GetCurSel();
switch (nCurSel)
{
case 0:
dw = (DWORD)THREAD_PRIORITY_IDLE;break;
case 1:
dw = (DWORD)THREAD_PRIORITY_LOWEST;break;
case 2:
dw = (DWORD)THREAD_PRIORITY_BELOW_NORMAL;break;
case 3:
default:
dw = (DWORD)THREAD_PRIORITY_NORMAL;break;
case 4:
dw = (DWORD)THREAD_PRIORITY_ABOVE_NORMAL;break;
case 5:
dw = (DWORD)THREAD_PRIORITY_HIGHEST;break;
case 6:
dw = (DWORD)THREAD_PRIORITY_TIME_CRITICAL;break;
}
if (nID == IDC_CNTRTHRDPRIORITY)
m_pCounterThread- >SetThreadPriority(dw);
//调整计数线程优先级
else
m_pDisplayThread- >SetThreadPriority(dw);
//调整显示线程优先级
}
这样线程优先级的调整只需要根据不同的ID来调用该函数:
void CMutexesDlg::OnSelchangeDspythrdpriority()
{ OnPriorityChange(IDC_DSPYTHRDPRIORITY);}
void CMutexesDlg::OnSelchangeCntrthrdpriority()
{ OnPriorityChange(IDC_CNTRTHRDPRIORITY);}
复选线程挂起的实现代码如下:
void CMutexesDlg::OnPause()
{
//取挂起复选框状态
CButton* pCheck = (CButton*)GetDlgItem(IDC_PAUSE);
BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0);
if (bPaused) {
m_pCounterThread- >SuspendThread();
m_pDisplayThread- >SuspendThread();
}//挂起线程
else {
m_pCounterThread- >ResumeThread();
m_pDisplayThread- >ResumeThread();
}//恢复线程运行
}
---- 程 序 在::OnClose() 中 实 现 了 线 程 的 终 止。 在 本 例 程 当 中 对 线 程 的 终 止 稍 微 复 杂 些。 需 要 注 意 的 是 成 员 变 量m_bDone 的 作 用, 在 线 程 的 运 行 当 中 循 环 检 测 该 变 量 的 状 态, 最 终 引 起 线 程 的 退 出。 这 样 线 程 的 终 止 是 因 为 函 数 的 退 出 而 自 然 终 止, 而 非 采 用 强 行 终 止 的 方 法, 这 样 有 利 于 系 统 的 安 全。 该 程 序 中 使 用 了PostMessage 函 数, 该 函 数 发 送 消 息 后 立 即 返 回, 这 样 可 以 避 免 阻 塞。 其 实 现 的 代 码 为:
void CMutexesDlg::OnClose()
{
int nCount = 0;
DWORD dwStatus;
//取挂起复选框状态
CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE);
BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0);
if (bPaused == TRUE){
pCheck- >SetCheck(0);//复选取消
m_pCounterThread- >ResumeThread();
//恢复线程运行
m_pDisplayThread- >ResumeThread();
}
if (m_pCounterThread != NULL){
VERIFY(::GetExitCodeThread(m_pCounterThread- >
m_hThread, &dwStatus));//取计数线程结束码
if (dwStatus == STILL_ACTIVE){
nCount++;
m_pCounterThread- >m_bDone = TRUE;
}//如果仍为运行状态,则终止
else{
delete m_pCounterThread;
m_pCounterThread = NULL;
}//如果已经终止,则删除该线程对象
}
if (m_pDisplayThread != NULL){
VERIFY(::GetExitCodeThread(m_pDisplayThread- >
m_hThread, &dwStatus));//取显示线程结束码
if (dwStatus == STILL_ACTIVE){
nCount++;
m_pDisplayThread- >m_bDone = TRUE;
}//如果仍为运行状态,则终止
else{
delete m_pDisplayThread;
m_pDisplayThread = NULL;
}//如果已经终止,则删除该线程对象
}
if (nCount == 0)//两个线程均终止,则关闭程序
CDialog::OnClose();
else //否则发送WM_CLOSE消息
PostMessage(WM_CLOSE, 0, 0);
}
---- 在 例 程 具 体 实 现 中 用 到 了 许 多 函 数, 在 这 里 不 一 一 赘 述, 关 于 函 数 的 具 体 意 义 和 用 法, 可 以 查 阅 联 机 帮 助。
用VC++5实现多线程
----多任务、多进程和多线程
----Windows95和WindowsNT操作系统支持多任务调度和处理,由此提供了多任务空间。程序员可控制应用程序中每一个片段的运行,从而编写高效率的应用程序。
----所谓多任务通常包括两大类:多进程和多线程。进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,这就是所谓基于多线程的多任务。
----开发多线程应用程序可利用32位Windows环境提供的Win32API接口函数,也可利用VC++中提供的MFC类库。多线程编程在这两种方式下原理是一样的,用户可以根据需要选择相应的工具。本文重点讲述用VC++5提供的MFC类库实现多线程调度与处理的方法,以及由线程多任务所引发的同步多任务特征,并给出一个实现多线程的例程。
----基于MFC的多线程编程
----1.MFC对多线程的支持
----MFC类库提供了多线程编程支持,使用户编程更加方便。重要的是,在多窗口线程情况下,MFC直接提供了用户接口线程的设计。
----MFC区分两种类型的线程:辅助线程(WorkerThread)和用户界面线程(UserInterfaceThread)。辅助线程没有消息机制,通常用来执行后台计算和维护任务。MFC为用户界面线程提供消息机制,用来处理用户的输入,响应用户产生的事件和消息。但对于Win32的API来说,这两种线程并没有区别,它只需要线程的启动地址以便启动线程执行任务。用户界面线程的一个典型应用就是CWinApp类,它是CWinThread类的派生类,提供应用程序的主线程,并负责处理用户产生的事件和消息。CWinThread类是用户接口线程的基本类,其对象用以维护特定线程的局部数据。因为处理线程局部数据依赖于CWinThread类,所以所有使用MFC的线程都必须由MFC来创建。例如,由run-time函数_beginthreadex创建的线程就不能使用任何MFCAPI。
----2.辅助线程和用户界面线程的创建和终止
----要创建一个线程,需要调用函数AfxBeginThread。该函数因参数重载不同而具有两种版本,分别对应辅助线程和用户界面线程。无论是辅助线程还是用户界面线程,都需要指定额外的参数以修改优先级、堆栈大小、创建标志和安全特性等。函数AfxBeginThread返回指向CWinThread类对象的指针。
----创建助手线程相对简单,只需实现控制函数和启动线程,而不必从CWinThread派生一个类。简要说明如下:
----(1)实现控制函数。控制函数定义该线程。当进入该函数,线程启动;退出时,线程终止。该控制函数声明如下:
----UINTMyControllingFunction(LPVOIDpParam);
----该参数是一个单精度32位值。该参数接收的值将在线程对象创建时传递给构造函数,控制函数将用某种方式解释该值。它可以是数量值,或是指向包括多个参数的结构的指针,甚至可以忽略。如果该参数是指结构,则不仅可以将数据从调用函数传给线程,也可以从线程回传给调用函数。如果使用这样的结构回传数据,当结果准备好时,线程要通知调用函数;当函数结束时,应返回一个UINT类型的值,指明结束的原因。通常,返回0表明成功,其他值分别代表不同的错误。
----(2)启动线程。由函数AfxBeginThread创建并初始化一个CWinThread类的对象,启动并返回该线程的地址,则线程进入运行状态。
----下面用简单的代码说明怎样定义一个控制函数以及如何在程序的其他部分使用。
UINTMyThreadProc(LPVOIDpParam)
{
CMyObject*pObject=(CMyObject*)pParam;
if(pObject==NULL||!pObject->IsKindOf(RUNTIME_CLASS(CMyObject)))
return-1;//非法参数
……//具体实现内容
return0;//线程成功结束
}
//在程序中调用线程的函数
……
pNewObject=newCMyObject;
AfxBeginThread(MyThreadProc,pNewObject);
……
----创建用户界面线程有两种方法。第一种方法,首先从CWinTread类派生一个类(注意:必须要用宏DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE对该类进行声明和实现);然后调用函数AfxBeginThread创建CWinThread派生类的对象进行初始化,启动线程运行。第二种方法,先通过构造函数创建类CWinThread的一个对象,然后由程序员调用函数::CreateThread来启动线程。通常CWinThread类的对象在该线程的生存期结束时将自动终止,如果程序员希望自己来控制,则需要将m_bAutoDelete设为FALSE。这样在线程终止之后,CWinThread类对象仍然存在,此时需要手动删除CWinThread对象。
----通常线程函数结束之后,线程将自行终止,CWinThread类将为我们完成结束线程的工作。如果在线程的执行过程中程序员希望强行终止线程的话,则需要在线程内部调用AfxEndThread(nExitCode),其参数为线程结束码。这样将终止线程的运行,并释放线程所占用的资源。如果从另一个线程来终止该线程,则必须在两个线程之间设置通信方法。如果从线程外部来终止线程,还可以使用Win32函数(CWinThread类不提供该成员函数):BOOLTerminateThread(HANDLEhThread,DWORDdwExitcode)。但在实际程序设计中对该函数的使用一定要谨慎,因为一旦该命令发出,将立即终止该线程,并不释放线程所占用的资源,这样可能会引起系统不稳定。
----如果所终止的线程是进程内的最后一个线程,则在该线程终止之后进程也相应终止。
----3.进程和线程的优先级
----在Windows95和WindowsNT操作系统中,任务是有优先级的,共有32级,从0到31,系统按照不同的优先级调度线程的运行。其中:
----(1)0~15级是普通优先级,线程的优先级可以动态变化。高优先级线程优先运行,只有高优先级线程不运行时,才调度低优先级线程运行。优先级相同的线程按照时间片轮流运行。
----(2)16~30级是实时优先级,实时优先级与普通优先级的最大区别在于,相同优先级进程的运行不按照时间片轮转,而是先运行的线程就先控制CPU,如果它不主动放弃控制,同级或低优先级的线程就无法运行。
----一个线程的优先级首先属于一个类,然后是其在该类中的相对位置。线程优先级的计算可以如下式表示:
----线程优先级=进程类基本优先级+线程相对优先级
----进程类的基本优先级有:
----IDLE_PROCESS_CLASS
----NORMAL_PROCESS_CLASS
----HIGH_PROCESS_CLASS
----REAL_TIME_PROCESS_CLASS
----线程的相对优先级有:
----THREAD_PRIORITY_IDLE
----(最低优先级,仅在系统空闲时执行)
----THREAD_PRIORITY_LOWEST
----THREAD_PRIORITY_BELOW_NORMAL
----THREAD_PRIORITY_NORMAL(缺省)
----THREAD_PRIORITY_ABOVE_NORMAL
----THREAD_PRIORITY_HIGHEST
----THREAD_PRIORITY_CRITICAL(非常高的优先级)
----4.线程同步
----编写多线程应用程序最重要的问题就是线程之间的资源同步访问,多个线程在共享资源时如果发生访问冲突,会产生不正确的结果。例如,一个线程正在更新一个结构的内容的同时,另一个线程正试图读取该结构。结果,我们将无法得知所读取的数据是什么状态。
----MFC提供了一组同步和同步访问类来解决这个问题。其中,同步对象包括:CSyncObject、CSemaphore、CMutex,CCriticalSection和CEvent;同步访问对象包括:CMultiLock和CSingleLock。
----同步类用于访问资源时保证资源的整体性。其中CSyncObject是其他四个同步类的基类,不直接使用。信号同步类CSemaphore通常用于当一个应用程序中同时有多个线程访问一个资源的情况(例如,应用程序允许对同一个Document有多个View);事件同步类CEvent通常用于在应用程序访问资源之前应用程序必须等待的情况(比如,在数据写进一个文件之前数据必须从通信端口得到);互斥同步类CMutex和临界区同步类CCriticalSection都是用于保证一个资源一次只能有一个线程访问,二者的不同之处在于前者允许有多个应用程序使用该资源,例如,该资源在一个DLL当中,而后者则不允许对同一个资源的访问超出进程的范畴,而且使用临界区的方式效率比较高。
----同步访问类用于获得对这些控制资源的访问。CMultiLock和CSingleLock的区别仅在于是需要控制访问多个还是单个资源对象。
----5.同步类的使用方法
----解决同步问题的一个简单方法是将同步类融入共享类中,通常我们把这样的共享类称为线程安全类。下面举例说明同步类的使用方法。比如,一个用以维护一个账户的连接列表的应用程序。该应用程序允许3个账户在不同的窗口中检测,但一次只能更新一个账户。当一个账户更新之后,需要将更新的数据通过网络传给一个数据文档。
----该例中将使用3种同步类。由于允许一次检测3个账户,可使用CSemaphore来限制对3个视窗对象的访问。当更新一个账目时,应用程序使用CCriticalSection来保证一次只有一个账目更新。在更新成功之后,发CEvent信号,该信号释放一个等待接收信号事件的线程,该线程将新数据传给数据文档。
----要设计一个线程安全类,首先根据具体情况在类中加入同步类作为数据成员。在本例中,可以将一个CSemaphore类的数据成员加入视窗类中,将一个CCriticalSection类数据成员加入连接列表类,而将一个CEvent数据成员加入数据存储类中。
----然后,在使用共享资源的函数中,将同步类与同步访问类的一个锁对象联系起来。即:在访问控制资源的成员函数中应该创建一个CSingleLock或CMultiLock的对象,并调用该对象的Lock函数。当访问结束之后,调用UnLock函数释放资源。
----用这种方式来设计线程安全类比较容易。在保证线程安全的同时,省去了维护同步代码的麻烦,这也正是OOP的思想。但是使用线程安全类方法编程比不考虑线程安全要复杂,尤其体现在程序调试过程中。而且线程安全编程还会损失一部分效率,比如在单CPU计算机中多个线程之间的切换会占用一部分资源。
----编程实例
----本文以VC++5中一个简单的基于对话框的MFC例程为例来说明实现多线程任务调度与处理的方法。
----该例程定义两个用户界面线程、一个显示线程(CDisplayThread)和一个计数线程(CCounterThread)。这两个线程同时操作一个字符串变量m_strNumber,其中显示线程将该字符串在一个列表框中显示,而计数线程则将该字符串中的整数加1。在例程中,可以分别调整进程、计数线程和显示线程的优先级。例程中的同步机制使用CMutex和CSingleLock来保证两个线程不能同时访问该字符串,同步机制执行与否将明显影响程序的执行结果。在该例程中允许把两个线程暂时挂起,以查看运行结果。例程中还允许查看计数线程的运行。该例程中所处理的问题也是多线程编程中非常具有典型意义的问题。
----该程序主要有三个用于调整优先级的组合框,分别用于选择同步机制、显示计数线程运行、挂起线程的复选框,以及显示运行结果的列表框。
----在本程序中使用了两个线程类CCounterThread和CDisplayThread,共同操作定义在CMutexesDlg中的字符串对象m_strNumber。本程序对同步类CMutex的使用方法就是按照本文所讲述的融入的方法来实现的。同步访问类CSingleLock的锁对象则在各线程的具体实现中定义。
----:程序的具体实现方法及代码发表在计算机世界WWW站点上,地址在http://www.computerworld.com.cn/98/skill/default.htm。
Windows95下多线程编程技术及其实现
问题的提出
笔者最近在开发基于Internet网上的可视电话过程中碰到了这样一个问题,即在基于In ternet网上的可视电话系统中,同时要进行语音采集、语音编译码、图像采集、图像编译码、语音和图像码流的传输,所有这些工作,都要并行处理。特别是语音信号,如果进行图像编解码时间过长,语音信号得不到服务,通话就有间断;如果图像或语音处理时间过长,而不能及时传输码流数据,通信同样也会中断。这样就要求我们实现一种并行编程,在只有一个CPU的机器上,也就是要将该CPU时间按时一定的优先准则分配给各个事件,定期处理各事件,而不会对某一事件处理过长。在32位Windows95或Windows NT下,我们可以用多线程的处理技术来实现这种并行处理。实际上,这种并行编程在很多场合下都是必须的。例如,在File Manager拷贝文件时,它显示一个对话框中包含了一个Cancel按钮。如果在文件拷贝过程中,点中Cance l按钮,就会终止拷贝。在16位Winows中,实现这类功能需要在File Copy循环内部周期性地调用PeekMessage函数。如果正在读一个很大的动作;如果从软盘读文件,则要花费好几秒的时间。由于机器反应太迟钝,用户会频繁地点中这个按钮,以为系统不知道想终止这个操作。如果把File Copy指令放入另外一个线程,就不需要在代码中放一大堆PeekMessage函数,处理用户界面的线程将与它分开操作,点中Cancel按钮后会立即得到响应。同样的道理,在应用程序中创建一个单独线程来处理所有打印任务也是很有用的,用户可以在打印处理时继续使用应用程序。
线程的概念
为了了解线程的概念 ,我们必须先讨论一下进程的概念。一个进程通常定义为程序的一个实例。在32位Windows中,进程占据4GB的虚拟地址空间。与它们在MS-DOS和16位Windows操作系统中不同,32位Windows进程是没有活力的。这就是说,一个32位Windows进程并不执行什么指令,它只是占据着4GB的地址空间,此空间中有应用程序EXE文件的代码和数据。
EXE需要的DLL也将它们的代码的数据装入到进程的地址空间。除了地址空间,进程还占有某些资源,比如文件、动态内存分配和线程。当进程终止时,在它生命期中创建的各种资源将被清除。
如上所述,进程是没有活力的,它只是一个静态的概念。为了让进程完成一些工作,进程必须至少占有一线程,所以线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含几个线程,它们可以同时执行进程的地址空间中的代码。为了做到这一点,每个线程有自己的一组CPU寄存器和椎。每个进程至少有一个线址程在执行其地址空间中的代码,如果没有线程执行进程地空间中的代码,如果没有线程执行进程地址空间中的代码,进程也就没有继续存在的理由,系统将自动清除进程及其地址空间。为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮转方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。创建一个32位Windows进程时,它的第一个线程称为主线程,由系统自动生成,然后可由这个主线程生成额外的线程,这些线程又可生成更多的线程。
线程的编程技术
1.编写线程函数
所有线程必须从一个指定的函数开始执行,该函数称为线程函数,它必须具有下列原型: DWORD WINAPI YourThreadFunc(LPVOID lpvT.hreadParm);
该函数输入一个LPVOID型的参数,可以是一个DWORD型的整数,也可以是一个指向一个缓冲区的指针,返回一个DWORD型的值。像WinMain函数一样,这个函数并不由操作系统调用,操作系统调用包含在KERNEL32.DLL中的非C运行时的一个内部函数,如StartOfThread,然后由S tartOfThread函数建立起一个异常处理框架后,调用我们的函数。
2.创建一个线程
一个进程的主线程是由操作系统自动生成,如果要让一个主线程创建额外的线程,可以调用CreateThread来完成。格式如下:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES jpsa.DWORD cbstack,LPTHREAD_START _ROUTINE lpStartAddr.LPVOID lpvThreadParm,DWORD fdwCreate,LPDWORD lpIDThread);
其中参数意义如下:
lpsa:是一个指向SECURITY_ATTRIBUTES结构的指针。如果想让对象为缺省安全属性的话,可以传一个NULL;如果想让任一个子进程都可继承该线程对象句柄,必须指定一个SECURITY _ATTRIBUTES结构,其中bInheritHandle成员初始化为TURE。
cbstark:表示线程为自己所用堆栈分配的地址空间大小,0表示采用系统缺省值。
lpStartAddr:表示新线程开始执行时代码所在函数的地址,即为线程函数。
lpvThreadParm:是传入线程函数的参数。
fdwCreate:指定控制线程创建的附加标志,可以取两种值。如果该参数为0,线程就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,初始化CPU,登记CONTEXT结构的成员,准备好执行该线程函数中的第一条指令,但并不马上执行,而是挂起该线程。
lpIDThrdad:是一个DWORD类型地址,返回赋给该新线程的ID值。
3.终止线程
如果某线程调用了ExitThread函数,就可以终止自己,如:
VOID ExtThead(UNIT fuExitCode);
这个函数为调用该函数的线程设置了退出码fuExitCode后,就终止该线程。
调用TerminateThread函数亦可终止线程。如:
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
该函数用来结束由hThread参数指定的线程,并把dwExitCode设成该线程的退出码。
当某个线程不再响应时,我们可以用其他线程调用该函数来终卡这个不响应的线程。
4.设定线程的相对优先级
当一个线程被首次创建时,它的优先级等同于它所属进程的优先级。在单个进程内可以通过调用SetThreadPrionrity函数改变线程的相对优先级。一个线程的优先级是相对于其所属的进程优先级而言的。
BOOL SetThreadPriority(HANDLE hThread,intnPriority);
其中参数hThread是指向待修改优先级线程的句柄,nPriority可以是以下的值:
THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIONRITY_ABOVE_NORMAL THREAD_PRIONITY_HIGHEST。
5.挂起及恢复线程
前文提到过可以创建挂起状态的线程,可以通过传递(CREATE_SUSPENDED标志给函数Cre ated来实现。当这样操作时,系统创建指定线程的核心对象,创建线程的栈,在CONTEXT结构中初始化线程CPU注册成员。然而,线程对象被分配了一个初始挂起计数值1,这表明系统将不再分配CPU去执行线程。要开始执行一个线程,另一个线程必须调用ResumeThread并传递给它调用CreateThread时返回的线程句柄。格式如下:
DWORD ResumeThread(HANDLE hThread);
一个线程可以被挂起多次。如果一个线程被挂起3次,则该线程在它被分配CPU之前必须被恢复3次。除了在创建线程时使用CREATE_SUSPENDED标志,还可以用SuspendThread函数挂起线程。格式如下:
DWORD SuspendThread(HANDLE hThread)。
多线程编程技术的应用
如前所述,为了实现基于TCP/IP下的可视电话,就必须"并行"地执行语音采集、语音编解码、图像采集、图像编解以及码流数据的接收与发送。语音与图像的采集由硬件采集卡进行,我们程序只需初始化该硬件采集卡,然后实时读取采集数据即可,但语音和图像数据的编解码以及码流数据的传输都必须由程序去协调执行,让CPU轮流为各个事件服务,决不能在某一件事件上处理过长。Windows 95下的线程正是满足这种要求的编程技术。
本文给出了利用Windows 95环境下用多线程编程技术实现的基于TCP/IP的可视电话的部分源码,其中包括主窗口过程函数以及主叫端与被叫端的TCP/IP接收线程函数和语音编解码的线程函数。由于图像编解码的实时性比语音处理与传输模块的实时性的要求要低些,所以以语音编解码为事件去查图像数据,然后进行图像编解码,而没有为图像编解码去单独实现一个线程。
在主窗口初始化时,用CREATE_SUSPENDED标志创建了两个线程hThreadG7231和hThreadT CPRev。一个用于语音编解码,它的线程函数为G723Proc,该线程不断查询本地有无编好码的语音和图像的码流,如有,则进行H.223打包,然后通过TCP的端口发送给对方。另外一个线程用于TCP/IP的接收,它的线程函数为AcceptThreadProcRiv,该线程不断侦测TCP/IP端口有无对方传来的码流,如有,就接收码流,进行H.223解码后送入相应的缓冲区。该缓冲区的内容, 由语音编解码线程G723Proc查询,并送入相应的解码器,由于使用了多线程的编程技术,使得操作系统定时去服务语音编解码模块和传输模块,从而保证了通信的不中断。
编者注:源程序发表在本报的WWW站点上,网址是:http://www.computerworld.co.cn/sk ill./skill.html,欢迎广大读者访问。
多线程编程应注意的问题
多线程编程应该注意的问题
从线程应用程序比单线程应用程序处理起来要更小心,因为是多个,每个线程都有自己的执行路线,不好控制,容易出问题。下面我们就看看在使用MFC进行多线程编程中应该注意的问题。
出于所占空间和效率的问题,MFC对象只在对象一级是安全的。你可以让两个线程处理两个数组,但不能让两个线程处理同一个数组,这可能就要出问题了。如果你必须使多个线程共同处理一个对象,必须使用适当的Win32同步机制。这方面的内容请参阅其它资料。类库使用临界区内部保护全局数据结束,如那些调试时使用的内存。如果你的应用程序中使用的线程不是使用CWinThread得到的,那就不能那个线程访问其它的MFC对象。
作为一个通用规则,线程只能访问它创建的MFC对象,这是因为临时的和永久的Windows句柄映射保存于线程本地存储中,这些句柄可以在多个线程访问数据时进行同步机制的保护。例如,工作线程不能执行一个计算,它调用文档的UpdateAllViews成员函数来更新所有的视,可是这并不能起作用,因为从CWnd对象到HWND的映射对主线程是局部的。这也就是说,一个线程可以拥有一个从Windows句柄到C++对象的映射,但不同的线程可以将同一个句柄映射到不同的C++对象,在一个线程中的改变不会影响到其它线程。
解决这个问题有几个办法,一个是传送进一个单独的句柄,而不是一个C++对象进工作线程。工作线程通过调用适当的FromHandle函数将这些对象加载到它的临时映射中去。当然也可以通过调用Attach加对象加到永久映射中去,但是这时你必须能够保证这个对象的生存时间比线程要长。
另一个办法是创建新的用户自定义消息,不同的线程使用不同的消息通知主程序什么事情发生了。
多线程程序设计
◆ 多线程简介
线程(thread)是操作系统分配 CPU 时间的基本实体。每一个应用程序至少有一个线程,也可以拥有多个线程。线程是程序中的代码流。多个线程可以同时运行,并能共享资源。
线程与进程不同,每个进程都需要操作系统为其分配独立的地址空间。而同一进程中的各个线程是在同一块地址空间中工作。
在 Java 程序中,一些动态效果(如动画的实现、动态的字幕等)常利用多线程技术来实现。
在 Java 语言中可以非常方便地使用多线程。和 Visual C++ 相比,Java 的多线程既容易学习,又容易使用。
◆ 创建多线程的两种办法:
(1)建立类 Thread 的子类
(2)实现接口 Rinnable
第二个办法比第一个使用得更为广泛。本讲座重点讲解第二个办法。
◆ 接口
Java 语言取消了 C++ 的多重继承(“多重继承”常常使 C++ 程序员陷入混乱之中)。Java 增加了“接口”(interface)的概念,使 Java 在取消多重继承后,并未使功能下降。
“接口”(interface)是一种特殊的类。当你定义一个类时,可以“实现”(implements)一个(或多个)接口。语法如下:
class 类名 extends 超类名 implements 接口名
◆ 例 1.5.1 一个最简单的多线程小应用程序
import java.applet.*;
import java.awt.*;
public class k04a extends Applet implements Runnable
{
private Thread m_k04a = null;
public k04a()
{
}
public void paint(Graphics g)
{
g.drawString("Running: " + Math.random(), 10, 20);
}
public void start()
{
m_k04a = new Thread(this);
m_k04a.start();
}
public void stop()
{
m_k04a.stop();
m_k04a = null;
}
public void run()
{
while (true)
{
try
{
repaint();
Thread.sleep(200);
}
catch (InterruptedException e)
{
stop();
}
}
}
}
◆ 控制线程的生命周期
(1)start()方法 启动一个线程
(2)run()方法 定义该线程的动作
(3)sleep()方法 使线程睡眠一段时间,单位为毫秒
(4)suspend()方法 使线程挂起
(5)resume()方法 恢复挂起的线程
(6)yield()方法 把线程移到队列的尾部
(7)stop()方法 结束线程生命周期并执行清理工作
(8)destroy()方法 结束线程生命周期但不做清理工作
其中最常用的是start(),run(),sleep(),stop()。
◆ try —— catch 语句
用于对“异常”的处理。和“错误”相比,“异常”是比较轻微的。它是指程序在运行中发生的意外情况。(try - catch 语句在 C++ 中也有)。
在执行 try 后面的语句时,如果发生异常,则执行 catch 后面的语句。
◆ 例 1.5.2 流动的标题
该程序在运行时,三个标题在由下而上不断变换。
HTML 文件中的写法:(三个图片要事先做好)
<APPLET CODE=testani.class WIDTH=400 HEIGHT=60>
<param name=image1 value="titl1.gif">
<param name=image2 value="titl2.gif">
<param name=image3 value="titl3.gif">
</APPLET>
JAVA 源程序:
import java.awt.*;
import java.applet.Applet;
public class testani extends Applet implements Runnable
{
Thread runner;
Image imgs[];
int high, y1, y2, y3;
public void init()
{
high = size().height;
y1 = high;
y2 = high*2;
y3 = high*3;
imgs = new Image[10];
for(int i=0;i<3;i++)
imgs[i]=getImage(getCodeBase(),getParameter("image"+(i+1)));
}
public void start()
{
runner = new Thread(this);
runner.start();
}
public void stop()
{
runner.stop();
runner = null;
}
public void run()
{
while (runner != null)
{
try
{
Thread.sleep(100);
repaint();
y1--;
if(y1==0)
{
Thread.sleep(3000);
y2=high;
}
y2--;
if(y2==0)
{
Thread.sleep(3000);
y3=high;
}
y3--;
if(y3==0)
{
Thread.sleep(3000);
y1 = high;
}
}
catch (InterruptedException e){}
}
}
public void paint(Graphics g)
{
g.drawImage(imgs[0], 0, y1, this);
g.drawImage(imgs[1], 0, y2, this);
g.drawImage(imgs[2], 0, y3, this);
}
public void update(Graphics g)
{
paint(g);
}
}
Visual C++ 5.0中的多线程编程技术
潘爱民
一、引言
Windows系统平台经历了从16位到32位的转变后,系统运行方式和任务管理方式有了很大的变化,在Windows 95和Windows NT中,每个Win32程序在独立的进程空间上运行,32位地址空间使我们从16位段式结构的64K段限制中摆脱出来,逻辑上达到了4G的线性地址空间。这样,我们在设计程序时就不再需要考虑编译的段模式,同时还提高了大程序的运行效率。独立进程空间的另一个更大的优越性是大大提高了系统的稳定性,一个应用程序的异常错误不会影响其它的应用程序,这对于现在的桌面环境尤为重要。
在Windows的一个进程内,包含一个或多个线程。线程是指进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件、信号标识及动态分配的内存等等。一个进程内的所有线程使用同一个32位地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行以及什么时候执行线程。线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完任务后再执行。在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样就可使处理器的任务平衡,也提高了系统的运行效率。
32位Windows环境下的Win32 API提供了多线程应用程序开发所需要的接口函数,但Win16和Win32对多线程应用并不支持,利用Visual C++ 5.0中提供的标准C库也可以开发多线程应用程序,而相应的MFC4.21类库则封装了多线程编程的类,因而用户在开发时可根据应用程序的需要和特点选择相应的工具。
如果用户的应用程序需要有多个任务同时进行相应的处理,则使用多线程是较理想的选择。例如,就网络文件服务功能的应用程序而言,若采用单线程编程方法,则需要循环检查网络的连接、磁盘驱动器的状况,并在适当的时候显示这些数据,必须等到一遍查询后才能刷新数据的显示。对使用者来说,延迟可能很长。而在应用多线程的情况下可将这些任务分给多个线程,一个线程负责检查网络,另一个线程管理磁盘驱动器,还有一个线程负责显示数据,三个线程结合起来共同完成文件服务,使用者也可以及时看到网络的变化。多线程应用范围很广,尤其是在目前的桌面平台上,系统的许多功能如网络(Internet)、打印、字处理、图形图像、动画和文件管理都在一个系统下运行,更需要我们的应用程序能够同时处理多个事件,而这些正是多线程可以实现的。本文讲述了利用Visual C++ 5.0进行多线程开发的编程技术。
二、基于Visual C++的多线程编程
Visual C++ 5.0提供了Windows应用程序的集成开发环境Developer Studio。在这个环境里,用户既可以编写C风格的32位Win32应用程序,也可以利用MFC类库编写C++风格的应用程序,二者各有其优点:基于Win32的应用程序执行代码小巧,运行效率高,但要求程序员编写的代码较多,且需要管理所有系统提供给程序的资源;而基于MFC类库的应用程序可以快速建立起应用程序,类库为程序员提供了大量的封装类,而且Developer Studio为程序员提供了一些工具来管理用户源程序,其缺点是类库代码很庞大,应用程序的执行代码离不开这些代码。由于使用类库所带来的快速、简捷和功能强大等优越性,因此,除非有特殊的需要,否则Visual C++提倡使用MFC类库进行应用程序开发。
多线程的编程在Win32方式下和MFC类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程。当线程执行完任务后,自动中止线程;当进程结束后,所有的线程都中止。所有活动的线程共享进程的资源。因此,在编程时需要考虑在多个线程访问同一资源时产生冲突的问题:当一个线程正在访问一个进程对象时,另一个线程要改变该对象,这时可能会产生错误的结果。所以,程序员编程时要解决这种冲突。
下面给大家介绍一下在Win32 基础上进行多线程编程的过程。
1.用Win32函数创建和中止线程
Win32函数库中提供了多线程控制的操作函数,包括创建线程、中止线程、建立互斥区等。首先,在应用程序的主线程或者其它活动线程的适当地方创建新的线程。创建线程的函数如下:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
其中,参数lpThreadAttributes 指定了线程的安全属性,在Windows 95中被忽略;dwStackSize 指定了线程的堆栈深度;lpStartAddress 指定了线程的起始地址,一般情况为下面的原型函数:DWORD WINAPI ThreadFunc( LPVOID );lpParameter指定了线程执行时传送给线程的32位参数,即上面函数的参数;dwCreationFlags指定了线程创建的特性; lpThreadId 指向一个DWORD变量,可返回线程ID值。
如果创建成功则返回线程的句柄,否则返回NULL。
创建了新的线程后,则该线程就开始启动执行了。如果在dwCreationFlags中用了CREATE_SUSPENDED特性,那么线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程,在这个过程中可以调用函数:
BOOL SetThreadPriority( HANDLE hThread, int nPriority);
来设置线程的优先权。
当线程的函数返回后,线程自动中止。如果在线程的执行过程中中止的话,则可调用函数:
VOID ExitThread( DWORD dwExitCode);
如果在线程的外面中止线程的话,则可调用下面的函数:
BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );
但应注意:该函数可能会引起系统不稳定,而且线程所占用的资源也不释放。因此,一般情况下,建议不要使用该函数。
如果要中止的线程是进程内的最后一个线程,则在线程被中止后相应的进程也应中止。
2.用Win32函数控制线程对共享资源的访问
在线程体内,如果该线程完全独立,与其它的线程没有数据存取等资源操作上的冲突,则可按照通常单线程的方法进行编程。但是,在多线程处理时情况常常不是这样,线程之间经常要同时访问一些资源。例如,一个线程负责公式计算,另一个线程负责结果的显示,两个线程都要访问同一个结果变量。这时如果不进行冲突控制的话,则很可能显示的是不正确的结果。
对共享资源进行访问引起冲突是不可避免的,但我们可用以下办法来进行操作控制:
(1) 通过设置线程的互斥体对象,在可能冲突的地方进行同步控制。
首先,建立互斥体对象,得到句柄:
HANDLE CreateMutex( );
然后,在线程可能冲突区域的开始(即访问共享资源之前),调用WaitForSingleObject将句柄传给函数,请求占用互斥体对象:
dwWaitResult = WaitForSingleObject(hMutex, 5000L);
共享资源访问完后,释放对互斥体对象的占用:
ReleaseMutex(hMutex);
互斥体对象在同一时刻只能被一个线程占用。当互斥体对象被一个线程占用时,若有另一线程想占用它,则必须等到前一线程释放后才能成功。
(2) 设置信号:在操作共享资源前,打开信号;完成操作后,关闭信号。这类似于互斥体对象的处理。
首先,创建信号对象:
HANDLE CreateSemaphore( );
或者打开一个信号对象:
HANDLE OpenSemaphore( );
然后,在线程的访问共享资源之前调用WaitForSingleObject。
共享资源访问完后,释放对信号对象的占用:
ReleaseSemaphore();
信号对象允许同时对多个线程共享资源的访问,在创建对象时指定最大可同时访问的线程数。当一个线程申请访问成功后,信号对象中的计数器减一;调用ReleaseSemaphore函数后,信号对象中的计数器加一。其中,计数器值大于等于0,小于等于创建时指定的最大值。利用信号对象,我们不仅可以控制共享资源的访问,还可以在应用的初始化时候使用。假定一个应用在创建一个信号对象时,将其计数器的初始值设为0,这样就阻塞了其它线程,保护了资源。待初始化完成后,调用ReleaseSemaphore函数将其计数器增加至最大值,进行正常的存取访问。
(3) 利用事件对象的状态,进行线程对共享资源的访问。
用ResetEvent函数设置事件对象状态为不允许线程通过;用SetEvent函数设置事件对象状态为可以允许线程通过。
事件分为手工释放和自动释放。如果是手工释放,则按照上述两函数处理事件的状态;如果是自动释放,则在一个线程结束后,自动清除事件状态,允许其它线程通过。
(4) 设置排斥区。在排斥区中异步执行时,它只能在同一进程的线程之间共享资源处理。虽然此时上面介绍的三种方法均可使用,但是,使用排斥区的方法则使同步管理的效率更高;
先定义一个CRITICAL_SECTION结构的排斥区对象,在进程使用之前先对对象进行初始化,调用如下函数:
VOID InitializeCriticalSection( LPCRITICAL_SECTION );
当一个线程使用排斥区时,调用函数:
EnterCriticalSection或者TryEnterCriticalSection
当要求占用、退出排斥区时,调用函数:
LeaveCriticalSection
释放对排斥区对象的占用,供其它线程使用。
互斥体对象、信号对象和事件对象也可以用于进程间的线程同步操作。在用Win32函数创建了对象时,我们可以指定对象的名字,还可以设置同步对象在子进程的继承性。创建返回的是HANDLE句柄,我们可以用函数DuplicateHandle来复制对象句柄,这样每个进程都可以拥有同一对象的句柄,实现进程之间的线程同步操作。另外,在同一进程内,我们可以用OpenMutex、OpenSemaphore和OpenEvent来获得指定名字的同步对象的句柄。
排斥区异步执行的线程同步方法只能用于同一进程的线程之间共享资源处理,但是这种方法的使用效率较高,而且编程也相对简单一些。
在Visual C++中,除了利用Win32函数进行多线程同步控制外,如果我们用到了MFC类库,则可利用已经封装成C++类结构的同步对象,使我们的编程更加简捷。
三、基于MFC的多线程编程
在Visual C++ 5.0附带的MFC 4.21类库中,也提供了多线程编程的支持,基本原理与上面所讲的基于Win32函数的设计一致,但由于MFC对同步对象作了封装,因此对用户编程实现来说更加方便,避免了对象句柄管理上的繁琐工作。更重要的是,在多个窗口线程情况下,MFC中直接提供了用户接口线程的设计。
在MFC中,线程分为两种:用户接口线程和辅助线程。用户接口线程常用于接收用户的输入,处理相应的事件和消息。在用户接口线程中,包含一个消息处理循环,其中CWinApp就是一个典型的例子,它从CWinThread派生出来,负责处理用户输入产生的事件和消息。辅助线程常用于任务处理(比如计算)不要求用户输入,对用户而言,它在后台运行。Win32 API并不区分这两种线程的类型,它只是获取线程的起始地址,然后开始执行线程。而MFC则针对不同的用户需要作了分类。如果我们需要编写多个有用户接口的线程的应用程序,则利用Win32 API要写很多的框架代码来完成每个线程的消息事件的处理,而用MFC则可以充分发挥MFC中类的强大功能,还可以使用ClassWizard来帮助管理类的消息映射和成员变量等,我们就可以把精力集中到应用程序的相关代码编写上。
辅助线程编程较为简单,设计的思路与上节所讲的基本一致:一个基本函数代表了一个线程,创建并启动线程后,则线程进入运行状态;如果线程用到共享资源,则需要进行资源同步处理。共享资源的同步处理在两种线程模式下完全一致。
我们知道:基于MFC的应用程序有一个应用对象,它是CWinApp派生类的对象,该对象代表了应用进程的主线程。当线程执行完(通常是接收到WM_QUIT消息)并退出线程时,由于进程中没有其它线程的存在,故进程也自动结束。类CWinApp从CWinThread派生出来,CWinThread是用户接口线程的基本类。我们在编写用户接口线程时,需要从CWinThread派生我们自己的线程类,ClassWizard可以帮助我们完成这个工作。
下面列出编写用户接口线程的基本步骤。
1.用ClassWizard派生一个新的类,设置基类为CWinThread
注意:类的DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE宏是必需的,因为创建线程时需要动态创建类的对象。根据需要可将初始化和结束代码分别放到类的InitInstance和ExitInstance函数中。如果需要创建窗口,则可在InitInstance函数中完成。
2.创建线程并启动线程
可以用两种方法来创建用户接口线程。
(1)MFC提供了两个版本的AfxBeginThread函数,其中一个用于创建用户接口线程,函数原型如下:
CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority, UINT nStackSize , DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs );
其中,参数pThreadClass指定线程的运行类,函数返回线程对象。
在创建线程时,可以指定线程先挂起,将参数dwCreateFlags设置为CREATE_SUSPENDED。然后,做一些初试工作,如对变量赋值等。最后,再调用线程类的ResumeThread函数启动线程。
函数AfxBeginThread的另一个版本指定一个线程函数并设置相应的参数,其它设置及用法与上述函数基本相同。
(2)我们也可以不用AfxBeginThread创建线程,而是分两步完成:首先,调用线程类的构造函数创建一个线程对象;其次,调用CWinThread::CreateThread函数来创建该线程。
注意:在这种情况下,在线程类中需要有公有的构造函数以创建其相应的C++对象。
线程建立并启动后,则线程在线程函数执行过程中一直有效。如果是线程对象,则在对象被删除之前,先结束线程。CWinThread已经为我们完成了线程结束的工作。
3. 同步对象的使用
不管是辅助线程还是用户接口线程,在存取共享资源时,都需要保护共享资源,以免引起冲突,造成错误。处理方法类似于Win32 API函数的使用,但MFC为我们提供了几个同步对象C++类,即CSyncObject、CMutex、CSemaphore、CEvent、CCriticalSection。这里,CSyncObject为其它四个类的基类,后四个类分别对应前面所讲的四个Win32 API同步对象。
通常,我们在C++对象的成员函数中使用共享资源,或者把共享资源封装在C++类的内部。我们可将线程同步操作封装在对象类的实现函数当中,这样在应用中的线程使用C++对象时,就可以像一般对象一样使用它,简化了使用部分代码的编写,这正是面向对象编程的思想。这样编写的类被称作“线程安全类”。在设计线程安全类时,首先应根据具体情况在类中加入一个同步对象类数据成员。然后,在类的成员函数中,凡是所有修改公共数据或者读取公共数据的地方均要加入相应的同步调用。一般的处理步骤是:创建一个CSingleLock或者CMultiLock对象,然后调用其Lock函数。当对象结束时,自动在析构函数中调用Unlock函数,当然也可以在任何希望的地方调用Unlock函数。
如果不是在特定的C++对象中使用共享资源,而是在特定的函数中使用共享资源(这样的函数称为“线程安全函数”),那么还是按照前面介绍的办法去做:先建立同步对象,然后调用等待函数,直到可以访问资源,最后释放对同步对象的控制。
下面我们讨论四个同步对象分别适用的场合:
(1)如果某个线程必须等待某些事件发生后才能存取相应资源,则用CEvent;
(2)如果一个应用同时可以有多个线程存取相应资源,则用CSemaphore;
(3)如果有多个应用(多个进程)同时存取相应资源,则用CMutex,否则用CCriticalSection。
使用线程安全类或者线程安全函数进行编程,比不考虑线程安全的编程要复杂,尤其在进行调试时情况更为复杂,我们必须灵活使用Visual C++提供的调试工具,以保证共享资源的安全存取。线程安全编程的另一缺点是运行效率相对要低些,即使在单个线程运行的情况下也会损失一些效率。所以,我们在实际工作中应具体问题具体分析,以选择合适的编程方法。
4. 多线程编程例程分析
上面讲述了在Visual C++ 5.0中进行多线程编程的技术要点,为了充分说明这种技术,我们来分析一下Visual C++提供的有关多线程的例程,看看一些多线程元素的典型用法。读者可运行这些例程,以获得多线程运行的直观效果。
(1)MtRecalc
例程MtRecalc的功能是在一个窗口中完成简单的加法运算,用户可输入加数和被加数,例程完成两数相加。用户可通过菜单选择单线程或用辅助线程来做加法运算。如果选择辅助线程进行加法运算,则在进行运算的过程中,用户可继续进行一些界面操作,如访问菜单、编辑数值等,甚至可以中止辅助运算线程。为了使其效果更加明显,例程在计算过程中使用了循环和延时,模拟一个复杂的计算过程。
在程序的CRecalcDoc类中,用到了一个线程对象和四个同步事件对象:
CWinThread* m_pRecalcWorkerThread;
HANDLE m_hEventStartRecalc;
HANDLE m_hEventRecalcDone;
HANDLE m_hEventKillRecalcThread;
HANDLE m_hEventRecalcThreadKilled;
当用户选择了菜单项Worker Thread后,多线程功能才有效。这时,或者选择菜单项Recalculate Now,或者在窗口中的编辑控制转移焦点时,都会调用函数:
void CRecalcDoc::UpdateInt1AndInt2(int n1, int n2, BOOL bForceRecalc);
在多线程的情况下,还会调用下面的CRecalcDoc::RecalcInSecondThread函数:
void CRecalcDoc::RecalcInSecondThread()
{
if (m_pRecalcWorkerThread == NULL)
{
m_pRecalcWorkerThread =
AfxBeginThread(RecalcThreadProc, &m_recalcThreadInfo);
}
m_recalcThreadInfo.m_nInt1 = m_nInt1;
m_recalcThreadInfo.m_nInt2 = m_nInt2;
POSITION pos = GetFirstViewPosition();
CView* pView = GetNextView(pos);
m_recalcThreadInfo.m_hwndNotifyRecalcDone = pView->m_hWnd;
m_recalcThreadInfo.m_hwndNotifyProgress = AfxGetMainWnd()->m_hWnd;
m_recalcThreadInfo.m_nRecalcSpeedSeconds = m_nRecalcSpeedSeconds;
SetEvent(m_hEventRecalcDone);
ResetEvent(m_hEventKillRecalcThread);
ResetEvent(m_hEventRecalcThreadKilled);
SetEvent(m_hEventStartRecalc);
}
上面加粗的语句是与多线程直接相关的代码,应用程序调用AfxBeginThread启动了线程,把m_recalcThreadInfo作为参数传给线程函数。函数中最后的四行语句设置了四个事件对象的状态,这些事件对象在文档类的构造函数中创建。下面是实际的运算线程函数:
UINT RecalcThreadProc(LPVOID pParam)
{
CRecalcThreadInfo* pRecalcInfo = (CRecalcThreadInfo*)pParam;
BOOL bRecalcCompleted;
while (TRUE)
{
bRecalcCompleted = FALSE;
if (WaitForSingleObject(pRecalcInfo->m_hEventStartRecalc, INFINITE)
!= WAIT_OBJECT_0)
break;
if (WaitForSingleObject(pRecalcInfo->m_hEventKillRecalcThread, 0)
WAIT_OBJECT_0)
break; // Terminate this thread by existing the proc.
ResetEvent(pRecalcInfo->m_hEventRecalcDone);
bRecalcCompleted = SlowAdd(pRecalcInfo->m_nInt1,
pRecalcInfo->m_nInt2,
pRecalcInfo->m_nSum,
pRecalcInfo,
pRecalcInfo->m_nRecalcSpeedSeconds,
pRecalcInfo->m_hwndNotifyProgress);
SetEvent(pRecalcInfo->m_hEventRecalcDone);
if (!bRecalcCompleted) // If interrupted by kill then...
break; // terminate this thread by exiting the proc.
::PostMessage(pRecalcInfo->m_hwndNotifyRecalcDone, WM_USER_RECALC_DONE, 0, 0);
}
if (!bRecalcCompleted)
SetEvent(pRecalcInfo->m_hEventRecalcThreadKilled);
return 0;
}
BOOL SlowAdd(int nInt1, int nInt2, int& nResult, CRecalcThreadInfo* pInfo, int nSeconds,HWND hwndNotifyProgress)
{
CWnd* pWndNotifyProgress = CWnd::FromHandle(hwndNotifyProgress);
BOOL bRestartCalculation = TRUE;
while (bRestartCalculation)
{
bRestartCalculation = FALSE;
for (int nCount = 1; nCount <20; nCount++)
{
if (pInfo != NULL
&& WaitForSingleObject(pInfo->m_hEventKillRecalcThread, 0) == WAIT_OBJECT_0)
{
if (hwndNotifyProgress != NULL)
{
pWndNotifyProgress->PostMessage( WM_USER_RECALC_IN_PROGRESS);
}
return FALSE; // Terminate this recalculation
}
if (pInfo != NULL
&&WaitForSingleObject(pInfo->m_hEventStartRecalc, 0) == WAIT_OBJECT_0)
{
nInt1 = pInfo->m_nInt1;
nInt2 = pInfo->m_nInt2;
bRestartCalculation = TRUE;
continue;
}
// update the progress indicator
Sleep(nSeconds * 50);
}
// update the progress indicator
}
nResult = nInt1 + nInt2;
return TRUE;
}
上面的代码充分显示了几个事件对象的用法。当线程刚启动时,先等待m_hEventStartRecalc的状态为允许,然后检查m_hEventKillRecalcThread事件对象的状态。注意这两个等待函数调用的第二个参数的区别:在进入计算函数之前,设置m_hEventRecalcDone事件为不允许状态;待计算结束后,将其设置为允许状态。在计算函数的处理过程中,循环检查事件m_hEventKillRecalcThread和m_hEventStartRecalc的状态,如果m_hEventKillRecalcThread事件允许,则退出线程,中止计算。
当计算线程在计算时,主线程可继续接受用户输入(包括菜单选择)。用户可通过菜单项中止计算线程。中止线程的处理比较简单,代码如下:
void CRecalcDoc::OnKillWorkerThread()
{
SetEvent(m_hEventKillRecalcThread);
SetEvent(m_hEventStartRecalc);
WaitForSingleObject(m_hEventRecalcThreadKilled, INFINITE);
m_pRecalcWorkerThread = NULL;
m_bRecalcInProgress = FALSE; // but m_bRecalcNeeded is still TRUE
UpdateAllViews(NULL, UPDATE_HINT_SUM);
}
通过设置m_hEventKillRecalcThread事件对象,计算线程的循环就会检测到该事件的状态,最终引起线程的退出。注意:线程的中止因函数的退出而自然中止,而没有用强行办法中止,这样可保证系统的安全性。另外,在程序的很多地方使用了PostMessage来更新计算进度的指示,使用PostMessage函数发送消息可立即返回,无需等待,这样就避免了阻塞,比较符合多线程编程的思想,建议读者使用这种消息发送方法。尤其是在多个UI线程编程时,用这种方法更合适。
(2)MtMDI
例程MtMDI是一个MDI应用,每一个子窗口是一个用户接口线程,子窗口里有一个来回弹跳的小球,小球的运动由计时器控制,此处不加以讨论。下面,我们来看看UI线程的创建过程以及它与MDI的结合。
通过菜单命令New Bounce,可在主框架窗口类中响应菜单命令,函数代码如下:
void CMainFrame::OnBounce()
{
CBounceMDIChildWnd *pBounceMDIChildWnd = new CBounceMDIChildWnd;
if (!pBounceMDIChildWnd->Create( _T("Bounce"),
WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, rectDefault, this))
return;
}
函数调用子框架窗口的创建函数,代码如下:
BOOL CBounceMDIChildWnd::Create(LPCTSTR szTitle, LONG style, const RECT& rect, CMDIFrameWnd* parent)
{
// Setup the shared menu
if (menu.m_hMenu == NULL)
menu.LoadMenu(IDR_BOUNCE);
m_hMenuShared = menu.m_hMenu;
if (!CMDIChildWnd::Create(NULL, szTitle, style, rect, parent))
return FALSE;
CBounceThread* pBounceThread = new CBounceThread(m_hWnd);
pBounceThread->CreateThread();
return TRUE;
}
当CBounceMDIChildWnd子窗口被删除时,Windows会同时删除CBounceWnd窗口(内嵌在线程对象pBounceThread中),因为它是CBounceMDIChildWnd的子窗口。由于CBounceWnd运行在单独的线程中,故当CBounceWnd子窗口被删除时,CWinThread线程对象也会自动被删除。
上述函数生成一个新的UI线程对象pBounceThread,并调用CreateThread函数创建线程。至此,线程已被创建,但还需要做初始化工作,如下面的函数InitInstance所示:
int CBounceThread::InitInstance()
{
CWnd* pParent = CWnd::FromHandle(m_hwndParent);
CRect rect;
pParent->GetClientRect(&rect);
BOOL bReturn = m_wndBounce.Create(_T("BounceMTChildWnd"),WS_CHILD | WS_VISIBLE, rect, pParent);
if (bReturn)
m_pMainWnd = &m_wndBounce;
return bReturn;
}
注意:这里,将m_pMainWnd设置为新创建的CBounceWnd窗口是必需的。只有这样设置了,才能保证当CBounceWnd窗口被删除时,线程会被自动删除。
(3)Mutexes
例程Mutexes是一个对话框程序。除主线程外,还有两个线程:一个用于计数,一个用于显示。在本例中,这两个线程都是从CWinThread派生出来的,但并不用于消息循环处理,派生类重载了Run函数,用于完成其计数和显示的任务。
在对话框类中使用了一个内嵌的CMutex对象。对话框初始化时创建两个线程,并设置相应的参数,然后启动运行两个线程。
当用户设置了对话框的同步检查框标记后,两个线程的同步处理有效。在计数线程的循环中,先调用CSingleLock::Lock函数,然后进行计数修改,最后调用CSingleLock::Unlock函数。注意:这里的CSingleLock对象根据主对话框的CMutex对象产生。在显示线程的循环中,先调用CSingleLock::Lock函数,然后取到计数值,最后调用CSingleLock::Unlock函数。注意:这里的CSingleLock对象也是由主对话框的CMutex对象产生。类似这种情况:一个线程要读取数据,另一个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过联机帮助来获取。
五、结束语
多线程函数是Win32不同于Win16的一个重要方面,其编程技术较为新颖,在程序设计思路上不同于传统的模块结构化方法,比一般的面向对象的思路也较为复杂,尤其是对于多处理器平台的处理更为复杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或不能同步处理的现象。
在其它的开发语言(如Visual Basic 5.0)中也提供了对多线程的支持,但从性能和安全的角度考虑,这种多线程支持受到较多的限制。不过,就一般应用而言,用这种处理方法已经足够了。
目前,大多数的计算机都是单处理器(CPU)的,在这种机器上运行多线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为了抢夺对CPU的控制权,则在线程切换时会消耗很多的CPU资源,但对于大部分时间被阻塞的线程(例如等待文件I/O操作),则可用一个单独的线程来完成。这样,就可将CPU时间让出来,使程序获得更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视具体情况加以处理,使应用程序获得最佳的性能。
关于线程
线程
线程是一个能独立于程序的其他部分运行的作业。线程属于一个过程,获得自己的CPU时间片。基于WIN32的应用程序可以使用多个可执行的线程,称为多线程。Windows 3.x不能提供一种机制天然地支持多线程应用程序,但是一些为Windows 3.x编写应用程序的公司使用他们自己的线程安排。
基于WIN32的应用软件能在给定的过程中产生多个线程。依靠生成多个线程,应用程序能够完成一些后台操作,例如计算,这样程序就能运行得更快。当线程运行时,用户仍能继续影响程序。正如前面谈到的,当一个应用程序运行时,就产生了一个相应的过程。那么应用程序就能有一个单独的线程等待键盘输入或执行一个操作,例如脱机打印或计算电子表格中各项的总数。
在网络世界中,当你试图调整你站点的服务器的性能时,就要运行线程。如果你使用的是IIS,你可以在服务器上设置对于每个处理器所能创建的线程的最大数目。这样,就能在处理器间更均匀地分配工作,从而加速你的站点。
线程模式
现在,为了让你知道线程是什么和在哪能使用他们,让我们看一下使用线程时你可能要运行的应用程序:ActiveX组件。ActiveX组件是独立于其他代码运行,基于COM的代码。这听起来是不是很熟悉?当你使用ActiveX组件时,必须在操作系统中注册。其中的一条注册信息就是,这个ActiveX组件是否支持多个线程,如果支持怎样支持。这就是线程模式。
组件支持的基本线程模式有:单线程,单元线程,组合线程。下面几个部分将谈谈每一种模式对组件来说意味着什么。
单线程
如果组件被标记(即注册)为单线程组件,这就意味着所有可执行函数(称作方法)都将在组件的一个共享线程中运行。这就类似于没有生成独立的可执行线程的应用程序。单线程组件的缺点是一次只能运行一个方法。如果多次调用组件,例如调用组件中的存储方法,就会产生瓶颈,因为一次只能有一个调用。
如果你正在创建或使用一个ActiveX组件,建议不要使用单线程组件。
单元线程
如果一个组件被标记为单元线程,那么每个可执行的方法都将在一个和组件相联系的线程上运行。之所以成为单元线程是因为,每个新生成的组件实例都有一个相应的线程单元,每个正在运行的组件都有它自己的线程。单元线程组件要比单线程组件要好,因为多个组件可以在各自的单元中同时运行方法。
自由线程
一个自由线程组件是一个支持多线程单元的多线程组件。这意味着多个方法调用可同时运行,因为每个调用都有自己的运行线程。这能使你的组件运行快得多,但也有一些缺点。运行在同一单元中的单元组件可以在单元中直接调用其他组件的方法,这是一个非常快的操作。但是,自由线程组件必须从一个单元向另一个单元调用。为了实现这一操作,WIN32生成了一个代理,用来通过单元界线。这对于每个需要的功能调用来说就产生了系统开销,从而减低了系统的速度。每一个访问自由组件的调用都有一个相应的代理。既然代理调用比直接调用慢,那么自然会有性能方面的降低。
关于自由线程组件另一个需要注意的是:他们不是真正自由的。如果你创建了一个自由线程组件。你仍必须确保组件中的线程完全同步。这不是一件容易的事。只是简单地把你的组件标记为是自由线程的,并不能使你的组件支持多线程,你仍要去做使你的组件自由线程化的工作。如果你不做这个工作,你的共享数据可能被破坏。这里说明一下为什么:让我们假定你有一个方法计算某个数然后把它写到某个变量中。此方法被传入一个初始值例如是4,在随后的计算中这个变量的值增长为5。在方法结束时这个最后的值被写入到变量中。如果一次只有一个计算过程的话,所有这些会工作得很好。然而,当数据正在被改变时,另一个线程试图访问它,那么重新得到的数据就有可能是错误的。
组合线程
读到这,你也许会想既然每种形式的线程都有自己的优点和缺点,为什么不把不同的线程模式结合起来使用呢?组合线程模式也许符合你的要求。一个被标记为组合线程的组件既有单元线程组件的特性又有自由线程组件的特性。当一个组件被标记为组合线程时,这个组件将总是在和生成它的对象所在单元相同的单元中创建。如果组件是被一个标记为单线程的对象创建的,那么这个组件的行为将和一个单元线程组件一样,并且它将在线程单元中创建。这就意味着,组件和创建它的对象之间的调用,不需要一个为通信提供的代理调用。
如果新组件是被自由线程组件创建的,那么这个组件将表现得像一个自由线程组件,但是它将在同一单元中运行,因此新组件能够直接访问创建它的对象(既不需代理调用)。切记,如果你打算把你的组件标记为组合线程,你必须提供线程同步保护你的线程数据。
[前一篇]: 解剖恶意网站代码:开机后弹出IE页面
[后一篇]: VB的热键技巧的终结篇
-- 收录于:2001年10月18日 4:57
-- 作者: -- 最后出处:http://www.chinaasp.com
采用多线程进行数据采集
作者:
评价:
上站日期: 2001年09月05日
内容说明:
来源:
数据采集技术在工业控制及自动化等领域中发挥着重要的作用。数据采集的一般过程是这样的:①向采集卡发出通道选择指令。②选择要采集的通道号。③启动A/D转换。④等待,直到转换完成。⑤从采集卡读出数据。对于多通道的采集,在程序的设计中,一般采用的两种方法。查询法或中断法。所谓查询方法就是采用一个循环,依次采集各个数据通道。查询法的优点是程序简单,易于实现;缺点是采集过程中,CPU多数时间是在等待,造成资源的浪费。中断法是采用硬件中断的形式——先启动A/D转换,在转换结束时发出一中断信号——CPU响应采集卡的中断时读出所采集的数据。这样,在等待转换的时间里,CPU可以进行其他的计算工作,而不用处于等待状态。中断法的优点是资源能充分利用;但是程序设计复杂,尤其是当系统的硬件中断资源紧张时,很容易造成中断冲突;另外,在Windows或Win95等操作系统中,不允许用户安装中断处理程序时,则无法实现。
以上讨论的两种方法都是在DOS下的方法;在Win95下,现在有了一个更好的方法——多线程技术。现在,我们可以利用多线程技术来进行数据采集。
1. 采用多线程进行数据采集的优点
Win95/98最让人喜爱的除了漂亮的界面以外,就是多线程与多任务了。DOS环境中,执行中的程序可以独占全部的资源;在Windows环境中,虽然它是一个略具雏形的多任务环境,但是只要你喜欢,你的程序仍然可以掌握所有的CPU时间。但是,在Windows 95以及Windows NT中,一个程序无法独占所有的CPU执行时间。而且,一个程序也不是从头到尾一条线。相反,一个程序在执行中可以分为多个程序片段,同时执行。这些能同时执行的程序片段称为线程。在Windows 95以及Windows NT中,操作系统同一时间可以轮流执行多个程序,这就是多任务。
采用多线程进行数据采集可以有效地加快程序的反应速度、增加执行的效率。一般的程序中都要处理用户的输入,但用户的输入速度与CPU的执行速度相比就向走路与做飞机一样。这样,CPU就将浪费大量的时间用来等待用户的输入(如在DOS环境中)。如果采用多线程,那么就可以用一个线程等待用户的输入;另一个线程进行数据处理或其他的工作。对于数据采集程序,可以用一个单独的线程进行数据采集。这样,能最大限度的保证采集的实时性,而另外的线程同时又能及时地响应用户的操作或进行数据处理。否则,程序在采集数据时就不能响应用户的操作;在响应用户操作时就不能进行数据采集。尤其当采集的数据量很大,数据处理任务很重时,如果不采用多线程,采集时的漫长的等待是很让人接受的。
但是,多线程要比普通程序设计复杂得多。由于任一时刻都可能有多个线程同时执行,所以,许多的变量、数据都可能会被其他线程所修改。这就是多线程程序中最关键的线程间的同步控制问题。
2. 多线程进行数据采集应解决的问题
其实,多线程程序设计复杂是暂时的;如果,你采用传统的C进行多线程的设计,那么你必须自己控制线程间的同步。那将是很复杂的。但是,如果利用面向对象的设计方法,采用Delphi进行多线程程序设计,问题就简单多了。这是因为,Delphi已将多线程的复杂性替我们处理了,我们所要做的就是继承。
具体地说,多线程数据采集需要完成以下工作:
① 从TThread类派生一个自己的类SampleThread。这就是我们用于数据采集的类。进行采集时,只需要简单地创建一个SampleThread的实例。
② 重载超类TThread的Execute方法。在这一方法中将具体地执行数据采集任务。
③ 如果希望一边采集一边显示,就在编写几个用于显示采集进度的过程,供Execute方法调用。
TThread类中最常用的属性/方法如下:
Create方法:constructor Create
(CreateSuspended: Boolean);
其中CreateSuspended参数确定线程在创建时是否立即执行。如果为True,新线程在创建后被挂起;如果为False,线程在创建后立即执行。
FreeOnTerminate属性:
property FreeOnTerminate: Boolean;
该属性确定程序员是否负责撤消该线程。如果该属性为True,VCL将在该线程终止时自动撤消线程对象。它的缺省值为False。
OnTerminate属性:
property OnTerminate: TNotifyEvent;
该属性指定一个当线程终止时发生的事件。
下面看一个具体的例子:
3. 多线程数据采集的实现
这是笔者开发的一个测抽油机功图的程序。它的功能是采集抽油机悬点的载荷及位移数据,经过处理后做出抽油机的功图。图1(略)所示是数据采集时的界面。点“采集数据”按钮后,程序将创建一新的线程,并设置其属性。这一新线程将完成数据采集任务。程序如下:
Procedure TsampleForm.
DoSampleBtnClick(Sender: TObject);
Begin
ReDrawBtn.Enabled := True;
DoSampleBtn.Enabled := False;
FFTBtn.Enabled := True;
TheSampler := SampleThread.Create(False);
创建采集线程
TheSampler.OnTerminate := FFTBtnClick;
采集完成后要执行的任务
TheSampler.FreeOnTerminate := True;
采集完成后撤消
End;
采集线程的类定义如下:
Type
SampleThread = class(TThread)
Public
function AdRead(ach: byte): integer; safecall;
读A/D卡的函数
procedure UpdateCaption;
显示采集所用时间
private
{ Private declarations }
protected
thes, thep: real;
dt: real;
id: integer;
st, ed: LongInt;
procedure Execute; override;
这是关键。
End;
在这个类中定义了一个函数AdRead用于操作A/D卡,两个过程用于显示采集的进度与所用时间。需要注意的是AdRead函数是用汇编写的,参数调用格式必须是safecall。
关键的重载方法Execute的代码如下:
Procedure SampleThread.Execute;
Begin
StartTicker := GetTickCount;
id := 0;
Repeat
thes := Adread(15) * ad2mv * mv2l;
采集第15通道
thep := Adread(3) * ad2mv * mv2n;
采集第3通道
dt := GetTickCount - StartTicker;
sarray[id] := thes;
parray[id] := thep;
tarray[id] := dt;
inc(id);
Synchronize(UpdateCaption);
注意:显示采集进度
Until id > =4096;
ed := GetTickCount;
Synchronize(ShowCostTime);
注意:显示所用时间
end;
从以上代码中可见,Execute与一般的代码并无本质区别。仅有的区别是显示采集进度和显示所用时间时,不能直接调用各自的过程,而是通过调用Synchronize间接地调用。这样作是为了保持进程间的同步。
4. 结论
以上的程序采用Delphi 4.0编程,在AMD-K6-2/300上实现。测试结果是这样的:采用多线程,采集4096个点一般耗用10~14s的时间;如果不采用多线程则需要1分钟到1分半。可见多线程可明显提高程序的执行效率。
循环创建多线程时保证参数的有效性
当我们需要在一个循环中传递参数时,使用使用函数的方法一般都是:
for(int I=0;I<100;I++){
fun(I); //使用函数传递i
}
每一个循环都会等待fun(I);函数执行完后再进行下一个循环。
但是当我们需要这个循环中创建线程,并将I的参数传递给线程时,如依然使用以上方法,会造成什么情况呢?
DWORD WINAPI ThreadFun(LPVOID lpParam){ //线程函数
Int *I = (int *)lpParam;
Return 0;
}
int I;
for(I=0;I<100;I++){
DWORD dwThreadId;
HANDLE hThread;
hThread = CreateThread(NULL,0,ThreadFunc,&I,0,&dwThreadId);
}
好了,到这里我们就可以发现,在循环中,我们创建线程并传递的参数是I=1后,主程序有可能在执到下一次循环时,第一次的ThreadFun函数仍未执行,而此时的I已经等2了,如果ThreadFun再来调用 Int *I = (int *)lpParam;语句时,显然不是我们想要的结果。
解决此问题的一种方法,便是可以使用静态数组来保存所要传递的参数。如下:
int I;
static int nPara[100]; //此句需定义为全局
for(I=0;I<100;I++){
DWORD dwThreadId;
HANDLE hThread;
NPara[I]=I; //保存参数
hThread = CreateThread(NULL,0,ThreadFunc,&nPara[I],0,&dwThreadId);
}
此时,所有参数均保存在nPara数组中,刚才的问题是解决了。
接下来又有了新的问题,让我们一起来看看吧:
1、如果需要创建的线程不止100,而是非常的大,而且我们也并不知道会有多少次循环的时侯。
2、如果我们需要传递的参数不单单只是一个int型的I,而是一个类。那么我们声明的时侯(假设线程数量最大为65535)则:
static CMYClass myClass[65535];
编译之后,得到的文件将会堆上一大堆的垃圾。相信任何一位程序都不想看到自己的程序上面堆了一堆垃圾在上面吧。
那么,还有没有更好的办法解决呢。答案是一定的,这里,我就讲一下我自己常用的方法:
动态创建对像传递参数。
一提到动态创建,我们自然会想到new 与 delete ,对了,我想说的也正是他们的使用。
假设参数类型为:
typedef struct _PARA{
int I;
DWORD dwNumber;
HWND hOther;
}Para;
使用new 在堆栈中申请一遍空间,在使用完后必需使用delete将其释放。
int I;
for(I=0;I<100;I++){
DWORD dwThreadId;
HANDLE hThread;
Para *myPara = new Para;
MyPara->I = I;
MyPara->dwNumber = 0 ;//自定
MyPara ->hOther = GetSafeHWnd();//当前窗体句柄
hThread = CreateThread(NULL,0,ThreadFunc,myPara,0,&dwThreadId);
}
//线程函数
DWORD WINAPI ThreadFun(LPVOID lpParam){ //线程函数
Para *myPara = (Para *)lpParam;
//执行其他功能
delete [] myPara; //释放
Return 0;
}
这样的话,也就不怕传递的参数多少与线程的数量太大了。另外如有需要的话可以加上一个线程计数器,保证当前线程的最大数量。
通常情况,我比较喜欢把线程处理放在一个类中处理,在主程序中尽量不与线程打交道。
以上是我在写ScanPort中遇到的一点点小问题,这里拿出来给大家一起分享,如果您有更好的方法,也请拿出与我们一起论讨。
AntGhazi/2001.12.21
mailto:antghazi@163.net
http://antghazi.yeah.net
MFC中多线程的应用
-------------------------------------------------------------------------------
我试着用自已的话来表述线程的概念,还有很短时间里编的一个小示例程序(不知恰当不?,也不知能说得清不..),见笑了.
线程其实和标准的windows主程序(WinMain)没啥两样...主程序其实是一个特殊的线程,称为主线程而已,其实你完全可以把线程想象成和winmain一起**同时运行**,但是** 可以相互访问(即在一个地址空间) **的一些小的WinMain程序.它和主线程一样,里面可以创建窗口,获取消息,等等..
由于线程们在一个地址空间且同时运行,所以会造成一些麻烦。因为我们编程都要用别人的函数库,而他们的函数库里面往往会有很多静态或全局的状态或中间变量,有着很复杂的相互依赖关系,如果执行某个功能不串行化(所谓串行化,也就是只能等一个功能调用返回后,另一个线程才能调用,不可以同时调用),就会造成大乱.这对线程来说,有术语称同步,windows为我们提供了很多同步的方法,MFC也提供了一些同步核心对象的类封装.对于某个功能调用库来说,叫线程安全.比如MFC的类库并不是线程安全的.
现在我举个刚刚自编的例子来简单说明这些概念。下面的一个对话框应用是多线程的.演示两个小动画:
(1)第一个动画由主线程的Timer来驱动,第二个动画由主线所创建的工作线程来驱动.分别显示在不同的位置.之所以我要加入Timer,也是为了形成线程驱动和timer驱动的对照,这是动画的两种驱动方式(还有在idle中驱动的)。
(2)这两个动画永远是不同的.也就是比如:一个是变哭,一个就会变笑,等那个变笑了,这个就变哭.动画图片来自于OICQ中的Face目录下,一般同样的头像会oicq会带三个图片(*-1.bmp,*-2.bmp,*-3.bmp),*-2.bmp是变灰的图片,我就取了1和3的图片来作动画.
这个程序的几个关键要注意的:
(1)主线程用PostThreadMessage和工作线程通信.工作线程用PeekMessage来取回消息。为了简单起见,我只用了一个WM_QUIT的消息来指示工作线程退出.
(2)主线程和工作线程同时调用了一个DisplayFace函数来进行动画显示.为了让两个动画一哭一笑做到不同,采用了CCriticalSection来进行同步.
示例如下:
(1)先用appwizards生成一个MFC的Dialog应用模板,假定对话框类为CTest01Dlg。
(2)再添入两个oicq的bmp文件到资源中去
(3)添加一个按钮(button)到对话框上.用作启动、停止动画的button
(4)用ClassWizard为button/onclick及dlg/ontimer生成事件响应函数,
(5)用Resource Symbol加入一个标识定义IDC_TIMER1
(6)在ClassView中为CTest01Dlg加入以下成员变量和成员函数
CriticalSection ccs;
CBitmap bm[2];
CWinThread* pMyThread;
static UINT MyThreadProc( LPVOID pParam);
void DisplayFace(CPoint r);
实现文件中加入相应代码(见下面)
(7)stdafx.h中加入#include
源代码如下,凡是我新加的代码周围都有注释包围,其它是ClassWizards自动写的:
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
file://
#if !defined(AFX_STDAFX_H__5B92DAA8_FE27_4702_8037_A2538343E69D__INCLUDED_)
#define AFX_STDAFX_H__5B92DAA8_FE27_4702_8037_A2538343E69D__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers
#include // MFC core and standard components
#include // MFC extensions
#include // MFC support for Internet Explorer 4 Common Controls
file://加入头引用主要是CCriticalSection对象的定义.
#include
file://加入结束
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include // MFC support for Windows Common Controls
#endif // _AFX_NO_AFXCMN_SUPPORT
file://{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_STDAFX_H__5B92DAA8_FE27_4702_8037_A2538343E69D__INCLUDED_)
// test01Dlg.h : header file
file://
#if !defined(AFX_TEST01DLG_H__F3780E23_CCFC_468C_A262_50FFF1D991BC__INCLUDED_)
#define AFX_TEST01DLG_H__F3780E23_CCFC_468C_A262_50FFF1D991BC__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
/////////////////////////////////////////////////////////////////////////////
// CTest01Dlg dialog
class CTest01Dlg : public CDialog
{
// Construction
public:
file://加入
CBitmap bm[2];
CCriticalSection ccs;
CWinThread* pMyThread;
static UINT MyThreadProc( LPVOID pParam);
void DisplayFace(CPoint r);
CTest01Dlg(CWnd* pParent = NULL); // standard constructor
file://加入结束
// Dialog Data
file://{{AFX_DATA(CTest01Dlg)
enum { IDD = IDD_TEST01_DIALOG };
// NOTE: the ClassWizard will add data members here
file://}}AFX_DATA
// ClassWizard generated virtual function overrides
file://{{AFX_VIRTUAL(CTest01Dlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
file://}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
file://{{AFX_MSG(CTest01Dlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnButton1();
afx_msg void OnTimer(UINT nIDEvent);
file://}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
file://{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_TEST01DLG_H__F3780E23_CCFC_468C_A262_50FFF1D991BC__INCLUDED_)
// test01Dlg.cpp : implementation file
file://
#include "stdafx.h"
#include "test01.h"
#include "test01Dlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialog
{
public:
CAboutDlg();
// Dialog Data
file://{{AFX_DATA(CAboutDlg)
enum { IDD = IDD_ABOUTBOX };
file://}}AFX_DATA
// ClassWizard generated virtual function overrides
file://{{AFX_VIRTUAL(CAboutDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
file://}}AFX_VIRTUAL
// Implementation
protected:
file://{{AFX_MSG(CAboutDlg)
file://}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
file://{{AFX_DATA_INIT(CAboutDlg)
file://}}AFX_DATA_INIT
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
file://{{AFX_DATA_MAP(CAboutDlg)
file://}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
file://{{AFX_MSG_MAP(CAboutDlg)
// No message handlers
file://}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CTest01Dlg dialog
CTest01Dlg::CTest01Dlg(CWnd* pParent /*=NULL*/)
: CDialog(CTest01Dlg::IDD, pParent)
{
file://{{AFX_DATA_INIT(CTest01Dlg)
// NOTE: the ClassWizard will add member initialization here
file://}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
file://加入
pMyThread =NULL;
file://加入结束
}
void CTest01Dlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
file://{{AFX_DATA_MAP(CTest01Dlg)
// NOTE: the ClassWizard will add DDX and DDV calls here
file://}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CTest01Dlg, CDialog)
file://{{AFX_MSG_MAP(CTest01Dlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON1, OnButton1)
ON_WM_TIMER()
file://}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CTest01Dlg message handlers
BOOL CTest01Dlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application''s main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
file://加入
bm[0].LoadBitmap (IDB_BITMAP1);
bm[1].LoadBitmap (IDB_BITMAP3);
file://加入结束
return TRUE; // return TRUE unless you set the focus to a control
}
void CTest01Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialog::OnSysCommand(nID, lParam);
}
}
void CTest01Dlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialog::OnPaint();
}
}
HCURSOR CTest01Dlg::OnQueryDragIcon()
{
return (HCURSOR) m_hIcon;
}
file://加入
void CTest01Dlg::OnButton1()
{
static BOOL bStarted=FALSE;
if (!bStarted){
SetTimer(IDC_TIMER1,500,NULL);
pMyThread=AfxBeginThread(MyThreadProc,this);
}else{
if (pMyThread){
pMyThread->PostThreadMessage (WM_QUIT,0,0);
::WaitForSingleObject(pMyThread->m_hThread ,INFINITE);
pMyThread=NULL;
}
KillTimer(IDC_TIMER1);
}
bStarted=!bStarted;
((CButton*)GetDlgItem(IDC_BUTTON1))->SetWindowText((bStarted?_T("停止"):_T("启动")));
}
void CTest01Dlg::OnTimer(UINT nIDEvent)
{
if (nIDEvent==IDC_TIMER1)
DisplayFace(CPoint(10,10));
CDialog::OnTimer(nIDEvent);
}
void CTest01Dlg::DisplayFace(CPoint p)
{
static int i=0;
ccs.Lock ();
BITMAP bmo;
bm[i].GetObject (sizeof(bmo),&bmo);
CClientDC dc(this);
CDC bmpDC;
bmpDC.CreateCompatibleDC (&dc);
bmpDC.SelectObject (&bm[i]);
dc.BitBlt (p.x ,p.y ,bmo.bmWidth,bmo.bmHeight,&bmpDC,0,0,SRCCOPY);
i++;
if (i==sizeof(bm)/sizeof(bm[0])) i=0;
ccs.Unlock ();
}
UINT CTest01Dlg::MyThreadProc(LPVOID pParam)
{
CTest01Dlg *me=(CTest01Dlg *)pParam;
MSG msg;
while(!PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
me->DisplayFace (CPoint(100,10));
::Sleep (200);
}
return 0;
}
file://加入结束
线程通信初探
进程是运行中的程序,有独立的内存,文件句柄和其它的系统资源,一个独立的进程可以包含多条执行路径,即线程。一个函数可以被多个线程访问,多个线程可以访问同一个全局变量。
Windows提供两种线程,用户界面线程和辅助线程。用户界面线程有窗口,因此有自己的消息循环,辅助线程没有窗口,不需要处理消息。但是辅助线程非常有用而且很容易编程,比如程序在某个运行时间要完成多个(很笨重的)任务时,显然,辅助线程的使用会使程序的运行效率大大的提高。但是,线程间的通信是一个必须解决的问题。
下面我们就来讨论一下线程间的通信的问题:
一.线程的管理
1.线程的启动:
在使用辅助线程时,我们必须为线程写一个全局函数,它的返回值必须为 UINT类型,而且必须有LPVOID类型的参数,启动线程调用下面的函数:
CWinThread* pThread=AfxBeginThread(
AFX_THREADPPOC ThreadProc,
LPVOID pParam,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs);
全局函数必须定义为 UINT ThreadProc(LPVOID pParam);
AfxBeginThread会立即返回一个指向新创建的线程对象的指针,用来管理线 程,包括挂起和恢复线程的运行,但是线程对象没有成员函数来中止线程的运 行。AfxBeginThread的第二个参数是一个32位的值,用来传给全局函数;第三 个参数用来设定线程的优先级;而第四和第六个参数用来指定线程堆栈大小和 安全性,一般采用默认值0;第五个参数用来设定创建线程对象的方式,0为立 即执行,CREATE_SUSPEND为线程通过ResumeThread后才执行。
而线程优先级的设置和获得可以通过下面的两个函数来实现:
pThread->SetThreadPriority(THREAD_PRIORITY_ABOVE_NOMAL);和
int nPriority=pThread->GetThreadPriority();
2.线程的中止:
可以调用MFC的AfxEndThread函数;
3.检查线程是否结束:
调用API函数GetExitCodeThread,
DWORD ExitCode ;
::GetExitCodeThread(pThread->m_hThread,&ExitCode );
if(ExitCode==STILL_ACTIVE)
//运行中
else //线程已经中止
二.主线程和辅助线程的通信
主线程和辅助线程间的通信方式有很多种,最简单的就是利用全局变量。 这里利用消息通信是行不通的,因为辅助线程没有消息循环,不能够利用 Windows消息。
下面我们用一个例子来说明。在例子中,我们写一个非常笨的函数,如实 现500*3000*3000的加法数据处理函数Add(int nCount);
我们在对话框上放置Start、Cancel按钮和一个用来表示数据处理进度的进度条 。
1.利用全局变量来实现主线程和辅助线程的通信:
我们编写全局函数如下:
UINT ThreadProc(LPVOID pParam)
{
nCount=0;//全局变量
while(nCount<500)
{
Add(nCount);
::InterlockedIncrement((long*)&ncount);
}
return 0;
}
函数InterlockIncrement阻塞其它的线程,当计数器递增时防止其它的线程访问nCount。
2.利用消息实现辅助线程和主线程的通信:
主线程有一个窗口,有消息循环,我们可以在调用AfxBeginThread时把窗口句柄传递给辅助线程,我们通过post方式传递消息,在函数退出时,给窗口发送一个消息。
重新编写线程函数如下:
int nCount=0;
UINT ThreadProc(LPVOID pParam)
{
while(nCount<500)
{
::InterlockedIncrement((long*)&ncount);
Add(nCount);
}
::PostMessage(
(HWND)pParam,
WM_THREADFINISHED,//用户自定义消息
0,0);
return 0;
}
编写OnStart函数:
void CThreadDlg::OnStart()
{
m_nTimer=SetTimer(1,100,NULL);//0.1秒
ASSERT(m_nTimer!=0);
GetDlgItem(IDC_START)->EnableWindow(FALSE);
AfxBeginThread(ThreadProc,GetSafeHwnd(),THREAD_PROIRITY_NOMAL);
}
编辑OnCancel函数如下:
void CThreadDlg::OnCancel()
{
if(nCount==0)
CDialog::OnCancel();
else nCount=500;
}
处理OnThreadFinished函数
HRESULT CThreadDlg::OnThreadFinished(WPARAM wParam,LPARAM lParam)
{
CDialog::OnOk();
return 0;
}
3.用事件使线程同步:
利用WaitForSingleObject函数
在stdafx.h中写入下面一行
#include <afxmt.h>//由于使用了事件
声明两个全局变量
CEvent m_start,m_kill;
在初始化函数中启动线程;
重新编写OnStart函数:
void CThreadDlg::OnStart()
{
m_nTimer=SetTimer(1,100,NULL);//0.1秒
ASSERT(m_nTimer!=0);
GetDlgItem(IDC_START)->EnableWindow(FALSE);
m_start.SetEvent();
}
重新编辑OnCancel函数如下:
void CThreadDlg::OnCancel()
{
if(nCount==0)
m_start.SetEvent();
else m_kill.SetEvent();
}
编写全局函数如下:
UINT ThreadProc(LPVOID pParam)
{
::WaitForSingleObject(m_start,INFINITE);
while(nCount<500)
{
Add(nCount);
if(::WaitForSingleObject(m_start,0)=WAIT_OBJECT_0)
break;
}
::PostMessage(
(HWND)pParam,
WM_THREADFINISHED,//用户自定义消息
0,0);
return 0;
}
其中第一个WaitForSingleObject的调用等待启动事件,INFINITE使其等待直到启动事件有信号。第二个调用若有信号,立即返回,中止线程。
VC++多线程下内存操作的优化
标题
VC++多线程下内存操作的优化 zdg(收藏)
关键字
VC++多线程下内存操作的优化
作者/李红亚
许多程序员发现用VC++编写的程序在多处理器的电脑上运行会变得很慢,这种情况多是由于多个线程争用同一个资源引起的。对于用VC++编写的程序,问题出在VC++的内存管理的具体实现上。以下通过对这个问题的解释,提供一个简便的解决方法,使得这种程序在多处理器下避免出现运行瓶颈。这种方法在没有VC++程序的源代码时也能用。
问题
C和C++运行库提供了对于堆内存进行管理的函数:C提供的是malloc()和free()、C++提供的是new和delete。无论是通过malloc()还是new申请内存,这些函数都是在堆内存中寻找一个未用的块,并且块的大小要大于所申请的大小。如果没有足够大的未用的内存块,运行时间库就会向操作系统请求新的页。页是虚拟内存管理器进行操作的单位,在基于Intel的处理器的NT平台下,一般是4,096字节。当你调用free()或delete释放内存时,这些内存块就返还给堆,供以后申请内存时用。
这些操作看起来不太起眼,但是问题的关键。问题就发生在当多个线程几乎同申请内存时,这通常发生在多处理器的系统上。但即使在一个单处理器的系统上,如果线程在错误的时间被调度,也可能发生这个问题。
考虑处于同一进程中的两个线程,线程1在申请1,024字节的内存的同时,运行于另外一个处理器的线程2申请256字节内存。内存管理器发现一个未用的内存块用于线程1,同时同一个函数发现了同一块内存用于线程2。如果两个线程同时更新内部数据结构,记录所申请的内存及其大小,堆内存就会产生冲突。即使申请内存的函数者成功返回,两个线程都确信自己拥有那块内存,这个程序也会产生错误,这只是个时间问题。
产生这种情况称为争用,是编写多线程程序的最大问题。解决这个问题的关键是要用一个锁定机制来保护内存管理器的这些函数,锁定机制保证运行相同代码的多个线程互斥地进行,如果一个线程正运行受保护的代码,则其他的线程都必须等待,这种解决方法也称作序列化。
NT提供了一些锁定机制的实现方法。CreateMutex()创建一个系统范围的锁定对象,但这种方法的效率最低;InitializeCriticalSection()创建的critical section相对效率就要高许多;要得到更好的性能,可以用具有service pack 3的NT 4的spin lock,更详细的信息可以参考VC++帮助中的InitializeCriticalSectionAndSpinCount()函数的说明。有趣的是,虽然帮助文件中说spin lock用于NT的堆管理器(HeapAlloc()系列的函数),VC++运行库的堆管理函数并没有用spin lock来同步对堆的存取。如果查看VC++运行库的堆管理函数的源程序,会发现是用一个critical section用于全部的内存操作。如果可以在VC++运行库中用HeapAlloc(),而不是其自己的堆管理函数,将会因为使用的是spin lock而不是critical section而得到速度优化。
通过使用critical section同步对堆的存取,VC++运行库可以安全地让多个线程申请和释放内存。然而,由于内存的争用,这种方法会引起性能的下降。如果一个线程存取另外一个线程正在使用的堆时,前一个线程就需要等待,并丧失自己的时间片,切换到其他的线程。线程的切换在NT下是相当费时的,因为其占用线程的时间片的一个小的百分比。如果有多个线程同时要存取同一个堆,会引起更多的线程切换,足够引起极大的性能损失。
现象
如何发现多处理器系统存在这种性能损失?有一个简便的方法,打开“管理工具”中的“性能”监视器,在系统组中添加一个上下文切换/秒计数,然后运行想要测试的多线程程序,并且在进程组中添加该进程的处理器时间计数,这样就可以得到处理器在高负荷下要发生多少次上下文切换。
在高负荷下有上千次的上下文切换是正常的,但当计数超过80,000或100,000时,说明过多的时间都浪费在线程的切换,稍微计算一下就可以知道,如果每秒有100,000次线程切换,则每个线程只有10微秒用于运行,而NT上的正常的时间片长度约有12毫秒,是前者的上千倍。
图1的性能图显示了过度的线程切换,而图2显示了同一个进程在同样的环境下,在使用了下面提供的解决方法后的情况。图1的情况下,系统每秒钟要进行120,000次线程切换,改进后,每秒钟线程切换的次数减少到1,000次以下。两张图都是在运行同一个测试程序时截取得,程序中同时有3个线程同时进行最大为2,048字节的堆的申请,硬件平台是一个双Pentium II 450机器,有256MB内存。
解决方法
本方法要求多线程程序是用VC++编写的,并且是动态链接到C运行库的。要求NT系统所安装的VC++运行库文件msvcrt.dll的版本号是6,所安装的service pack的版本是5以上。如果程序是用VC++ v6.0以上版本编译的,即使多线程程序和libcmt.lib是静态链接,本方法也可以使用。
当一个VC++程序运行时,C运行库被初始化,其中一项工作是确定要使用的堆管理器,VC++ v6.0运行库既可以使用其自己内部的堆管理函数,也可以直接调用操作系统的堆管理函数(HeapAlloc()系列的函数),在__heap_select()函数内部分执行以下三个步骤:
1、检查操作系统的版本,如果运行于NT,并且主版本是5或更高(Window 2000及以后版本),就使用HeapAlloc()。
2、查找环境变量__MSVCRT_HEAP_SELECT,如果有,将确定使用哪个堆函数。如果其值是__GLOBAL_HEAP_SELECTED,则会改变所有程序的行为。如果是一个可执行文件的完整路径,还要调用GetModuleFileName()检查是否该程序存在,至于要选择哪个堆函数还要查看逗号后面的值,1表示使用HeapAlloc(),2表示使用VC++ v5的堆函数,3表示使用VC++ v6的堆函数。
3、检测可执行文件中的链接程序标志,如果是由VC++ v6或更高的版本创建的,就使用版本6的堆函数,否则使用版本5的堆函数。
那么如何提高程序的性能?如果是和msvcrt.dll动态链接的,保证这个dll是1999年2月以后,并且安装的service pack的版本是5或更高。如果是静态链接的,保证链接程序的版本号是6或更高,可以用quickview.exe程序检查这个版本号。要改变所要运行的程序的堆函数的选取,在命令行下键入以下命令:
set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1
以后,所有从这个命令行运行的程序,都会继承这个环境变量的设置。这样,在堆操作时都会使用HeapAlloc()。如果让所有的程序都使用这些速度更快的堆操作函数,运行控制面板的“系统”程序,选择“环境”,点取“系统变量”,输入变量名和值,然后按“应用”按钮关闭对话框,重新启动机器。
按照微软的说法,可能有一些用VC++ v6以前版本编译程序,使用VC++ v6的堆管理器会出现一些问题。如果在进行以上设置后遇到这样的问题,可以用一个批处理文件专门为这个程序把这个设置去掉,例如:
set __MSVCRT_HEAP_SELECT=c:/program files/myapp/myapp.exe,1 c:/bin/buggyapp.exe,2
测试
为了验证在多处理器下的效果,编了一个测试程序heaptest.c。该程序接收三个参数,第一个参数表示线程数,第二个参数是所申请的内存的最大值,第三个参数每个线程申请内存的次数。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
/* compile with cl /MT heaptest.c */
/* to switch to the system heap issue the following command
before starting heaptest from the same command line
set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */
/* structure transfers variables to the worker threads */
typedef struct tData
{
int maximumLength;
int allocCount;
} threadData;
void printUsage(char** argv)
{
fprintf(stderr,"Wrong number of parameters./nUsage:/n");
fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n",
argv[0]);
exit(1);
}
unsigned __stdcall workerThread(void* myThreadData)
{
int count;
threadData* myData;
char* dummy;
srand(GetTickCount()*GetCurrentThreadId());
myData=(threadData*)myThreadData;
/* now let us do the real work */
for(count=0;count<myData->allocCount;count++)
{
dummy=(char*)malloc((rand()%myData->maximumLength)+1);
free(dummy);
}
_endthreadex(0);
/* to satisfy compiler */
return 0;
}
int main(int argc,char** argv)
{
int threadCount;
int count;
threadData actData;
HANDLE* threadHandles;
DWORD startTime;
DWORD stopTime;
DWORD retValue;
unsigned dummy;
/* check parameters */
if(argc<4 || argc>4)
printUsage(argv);
/* get parameters for this run */
threadCount=atoi(argv[1]);
if(threadCount>64)
threadCount=64;
actData.maximumLength=atoi(argv[2])-1;
actData.allocCount=atoi(argv[3]);
threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));
printf("Test run with %d simultaneous threads:/n",threadCount);
startTime=GetTickCount();
for(count=0;count<threadCount;count++)
{
threadHandles[count]=(HANDLE)_beginthreadex(0,0,
&workerThread, (void*)&actData,0,&dummy);
if(threadHandles[count]==(HANDLE)-1)
{
fprintf(stderr,"Error starting worker threads./n");
exit(2);
}
}
/* wait until all threads are done */
retValue=WaitForMultipleObjects(threadCount,threadHandles
,1,INFINITE);
stopTime=GetTickCount();
printf("Total time elapsed was: %d milliseconds",
stopTime-startTime);
printf(" for %d alloc operations./n",
actData.allocCount*threadCount);
/* cleanup */
for(count=0;count<threadCount;count++)
CloseHandle(threadHandles[count]);
free(threadHandles);
return 0;
}
测试程序在处理完参数后,创建参数1指定数量的线程,threadData结构用于传递计数变量。workThread中进行内存操作,首先初始化随机数发生器,然后进行指定数量的malloc()和free()操作。主线程调用WaitForMultipleObject()等待工作者线程结束,然后输出线程运行的时间。计时不是十分精确,但影响不大。
为了编译这个程序,需要已经安装VC++ v6.0程序,打开一个命令行窗口,键入以下命令:
cl /MT heaptest.c
/MT表示同C运行库的多线程版静态链接。如果要动态链接,用/MD。如果VC++是v5.0的话并且有高版本的msvcrt.dll,应该用动态链接。现在运行这个程序,用性能监视器查看线程切换的次数,然后按上面设置环境参数,重新运行这个程序,再次查看线程切换次数。
当截取这两张图时,测试程序用了60,953ms进行了3,000,000次的内存申请操作,使用的是VC++ v6的堆操作函数。在转换使用HeapAlloc()后,同样的操作仅用了5,291ms。在这个特定的情况下,使用HeapAlloc()使得性能提高了10倍以上!在实际的程序同样可以看到这种性能的提升。
结论
多处理器系统可以自然提升程序的性能,但如果发生多个处理器争用同一个资源,则可能多处理器的系统的性能还不如单处理器系统。对于C/C++程序,问题通常发生在当多个线程进行频繁的内存操作活动时。如上文所述,只要进行很少的一些设置,就可能极大地提高多线程程序在多处理器下的性能。这种方法即不需要源程序,也不需要重新编译可执行文件,而最大的好处是用这种方法得到的性能的提高是不用支付任何费用的。
任务,过程,和线程
标题
任务,过程,和线程 ghj1976(转贴)
关键字
任务,过程,和线程
出处
http://www.microsoft.com/china/msdn/techvoice/09.asp
在你很小的时候,你学习怎样一次完成一个工作,但到了成年,当然也许你已成为父母辈了,这时,你就必须学会如何在同一时间做多个工作。例如,你是否发现有多少次你在办公室里作弄过电话,E-MAIL 和客人?你也许正在办公室里和一个人在谈话,这时电话铃响了,谈话就被电话打断了。也许正在打电话时,你又来了新(重要的)E-MAIL,你必须中断你的电话。一旦处理完E-MAIL以后,你可以接着打电话或者继续和办公室里的人谈话。如果等的时间太长了,电话另一端的人可能会挂断电话,或者办公室里的人也许会生气离开。你必须决定如何花费你的时间和花在谁身上。
在上面的例子中,你就是在进行多任务处理。你决定如何花费你的时间和计算机对多任务进行时序安排类似。正如我们一天只有24小时,计算机的资源也是有限的。看起来好像你在同时打电话和处理E-MAIL,但事实上,每次你只能将注意力集中在一件事情上。实际上你是在不断变换注意力,只不过变换得足够快,就好像你在同时做多个工作一样。当计算机处理多任务时,它从一个程序到另一个程序的切换是非常快的,以至于使你认为,所有 的程序都在同时运行。
图一,从用户观点看单任务和多任务
假如你有一个克隆(可能叫多莉)的话,你就能同时做两件事。对于有多个CPU的计算机,同时在每一个CPU上运行程序称为多重处理。有时,人们可替换使用多任务处理和多重处理两个概念,但是,你不能在没有多个处理器的计算机上进行多重处理。因此,如果你正在使用一台只有一个处理器的计算机,操作系统可以进行多任务处理,如果你正在使用一台有多个处理器的计算机,操作系统既能进行多任务处理又能进行多重处理。
在操作系统中,过程是一个逻辑上的任务。过程是在运行应用程序,启动某一系统服务,和在Windows NT中启动某一子系统时产生的。每一个过程都有自己的专用资源(例如自己的专用存储空间),只有拥有这个过程的应用程序才能访问这些资源。这意味着,如果你产生了一个程序,在程序中用到一些数据,并且你也没有建立任何形式的程序间数据共享机制或使用操作系统的程序间数据共享机制,那么就只有你的程序能访问这些数据。大多数Windows开发者都用COM对象简化程序间的通信。如果你用的是Windows NT并且想共享数据,你可以使用内存映像文件。但是这对于Geek Speak column来说技术性太强,所以让我们继续往下讲。
多任务处理:合作的和有优先权的
你能使用的多任务处理有两种基本方法:合作的,在这种方法中,正在运行的过程必须为其他过程留出CPU时间片;有优先权的,在这种方法中,操作系统决定哪个程序获得时间片。Microsoft Windows 3.x和Macintosh用的都是合作的多任务处理,而OS/2,Windows 95,Windows NT,UNIX,和Amiga操作系统使用的是有优先权的多任务处理。
合作的多任务处理
如果使用合作的多任务处理,每个程序必须允许其他程序使用CPU。使用合作的多任务处理系统的应用软件都有一个特殊的码环,这个码环产生控制允许其他应用软件的运行。如果每个人都按规则办事,这种方法会工作得相当好,但是当应用程序不服从这一规则时,他“霸占”CPU。这意味着,终端用户不能转向其他应用程序,使操作系统或应用程序出现“挂起”。
有优先权的多任务处理
在有优先权的多任务处理中,操作系统安排CPU时间,一个应用软件在任何时候都有可能被操作系统暂停(先占)。这减轻了“一玩到底”的程序问题,因为操作系统负责分给每个应用软件自己的时间片。
Windows 95对于32位Windows应用程序采用有优先权的多任务处理,为了能够向下兼容,对于16位的Windows应用程序(为Windows 3.x写的应用程序)仍采用合作的多任务处理。
线程
线程是一个能独立于程序的其他部分运行的作业。线程属于一个过程,获得自己的CPU时间片。基于WIN32的应用程序可以使用多个可执行的线程,称为多线程。Windows 3.x不能提供一种机制天然地支持多线程应用程序,但是一些为Windows 3.x编写应用程序的公司使用他们自己的线程安排。
基于WIN32的应用软件能在给定的过程中产生多个线程。依靠生成多个线程,应用程序能够完成一些后台操作,例如计算,这样程序就能运行得更快。当线程运行时,用户仍能继续影响程序。正如前面谈到的,当一个应用程序运行时,就产生了一个相应的过程。那么应用程序就能有一个单独的线程等待键盘输入或执行一个操作,例如脱机打印或计算电子表格中各项的总数。
在网络世界中,当你试图调整你站点的服务器的性能时,就要运行线程。如果你使用的是IIS,你可以在服务器上设置对于每个处理器所能创建的线程的最大数目。这样,就能在处理器间更均匀地分配工作,从而加速你的站点。
线程模式
现在,为了让你知道线程是什么和在哪能使用他们,让我们看一下使用线程时你可能要运行的应用程序:ActiveX组件。ActiveX组件是独立于其他代码运行,基于COM的代码。这听起来是不是很熟悉?当你使用ActiveX组件时,必须在操作系统中注册。其中的一条注册信息就是,这个ActiveX组件是否支持多个线程,如果支持怎样支持。这就是线程模式。
组件支持的基本线程模式有:单线程,单元线程,组合线程。下面几个部分将谈谈每一种模式对组件来说意味着什么。
单线程
如果组件被标记(即注册)为单线程组件,这就意味着所有可执行函数(称作方法)都将在组件的一个共享线程中运行。这就类似于没有生成独立的可执行线程的应用程序。单线程组件的缺点是一次只能运行一个方法。如果多次调用组件,例如调用组件中的存储方法,就会产生瓶颈,因为一次只能有一个调用。
如果你正在创建或使用一个ActiveX组件,建议不要使用单线程组件。
单元线程
如果一个组件被标记为单元线程,那么每个可执行的方法都将在一个和组件相联系的线程上运行。之所以成为单元线程是因为,每个新生成的组件实例都有一个相应的线程单元,每个正在运行的组件都有它自己的线程。单元线程组件要比单线程组件要好,因为多个组件可以在各自的单元中同时运行方法。
自由线程
一个自由线程组件是一个支持多线程单元的多线程组件。这意味着多个方法调用可同时运行,因为每个调用都有自己的运行线程。这能使你的组件运行快得多,但也有一些缺点。运行在同一单元中的单元组件可以在单元中直接调用其他组件的方法,这是一个非常快的操作。但是,自由线程组件必须从一个单元向另一个单元调用。为了实现这一操作,WIN32生成了一个代理,用来通过单元界线。这对于每个需要的功能调用来说就产生了系统开销,从而减低了系统的速度。每一个访问自由组件的调用都有一个相应的代理。既然代理调用比直接调用慢,那么自然会有性能方面的降低。
关于自由线程组件另一个需要注意的是:他们不是真正自由的。如果你创建了一个自由线程组件。你仍必须确保组件中的线程完全同步。这不是一件容易的事。只是简单地把你的组件标记为是自由线程的,并不能使你的组件支持多线程,你仍要去做使你的组件自由线程化的工作。如果你不做这个工作,你的共享数据可能被破坏。这里说明一下为什么:让我们假定你有一个方法计算某个数然后把它写到某个变量中。此方法被传入一个初始值例如是4,在随后的计算中这个变量的值增长为5。在方法结束时这个最后的值被写入到变量中。如果一次只有一个计算过程的话,所有这些会工作得很好。然而,当数据正在被改变时,另一个线程试图访问它,那么重新得到的数据就有可能是错误的。下面的图表说明了这一点。
为了修正这一错误,开发者为对象提供了线程同步。线程同步是在正在运行你想保护的某一其他代码时运行的代码。操作系统并不先占这个代码,直到获得一个可以中断的信号。如果你想了解更多的有关线程同步对象的详细内容,你不应该阅读Geek Speak column!不,我的意思是,“注意看一下本文后面列出的参考阅读文献”。
图二,共享数据被多线程访问搞乱了
组合线程
读到这,你也许会想既然每种形式的线程都有自己的优点和缺点,为什么不把不同的线程模式结合起来使用呢?组合线程模式也许符合你的要求。一个被标记为组合线程的组件既有单元线程组件的特性又有自由线程组件的特性。当一个组件被标记为组合线程时,这个组件将总是在和生成它的对象所在单元相同的单元中创建。如果组件是被一个标记为单线程的对象创建的,那么这个组件的行为将和一个单元线程组件一样,并且它将在线程单元中创建。这就意味着,组件和创建它的对象之间的调用,不需要一个为通信提供的代理调用。
如果新组件是被自由线程组件创建的,那么这个组件将表现得像一个自由线程组件,但是它将在同一单元中运行,因此新组件能够直接访问创建它的对象(既不需代理调用)。切记,如果你打算把你的组件标记为组合线程,你必须提供线程同步保护你的线程数据。
更多的信息
到此为止,你应该对过程,作业,线程有一个基本的了解了。如果你想进一步了解,下面这些文章也许对你有用。我必须事先提醒你,这些资料中的大多数并不是为初学者准备的。很好地理解COM,C++,和WIN32,将会对理解某些文章大有帮助。
- Multithreading for Rookies ( http://msdn.microsoft.com/library/techart/msdn_threads.htm) by Ruediger Asche
- Windows 95 Resource Kit, Chapter 31 The Windows 95 Architecture, Virtual Machine Manager, Process Scheduling and Multitasking ( http://msdn.microsoft.com/library/winresource/dnwin95/d1c/s72bd.htm)
- Understanding and Using COM Threading Models ( http://www.microsoft.com/workshop/components/com/comthread.asp)
- Descriptions and Workings of OLE Threading Models ( http://support.microsoft.com/support/kb/articles/q150/7/77.asp)
- Fashionable App Designers Agree: The Free-Threading Model is What抯 Hot This Fall ( http://www.microsoft.com/msj/0897/free.htm)
- IIS Documentation: Selecting a Threading Mode ( http://msdn.microsoft.com/library/sdkdoc/iisref/eadg2pbg.htm)
使用临界段实现优化的进程间同步对象-原理和实现
by Jeffrey.Richter
vcbear 热情讲解
实现自己的同步对象?需要吗?
不需要吗?
...
只是跟你研究一下而已.
算了吧我只是个爱灌水的家伙,很久没有写代码了,闲来无事,灌灌水还不行吗?
1.概述:
在多进程的环境里,需要对线程进行同步.常用的同步对象有临界段(Critical Section),互斥量(Mutex),信号量(Semaphore),事件(Event)等,除了临界段,都是内核对象。
在同步技术中,临界段(Critical Section)是最容易掌握的,而且,和通过等待和释放内核态互斥对象实现同步的方式相比,临界段的速度明显胜出.但是临界段有一个缺陷,WIN32文档已经说明了临界段是不能跨进程的,就是说临界段不能用在多进程间的线程同步,只能用于单个进程内部的线程同步.
因为临界段只是一个很简单的数据结构体,在别的进程的进程空间里是无效的。就算是把它放到一个可以多进程共享的内存映象文件里,也还是无法工作.
有甚么方法可以跨进程的实现线程的高速同步吗?
2.原理和实现
2.1为什么临界段快? 是“真的”快吗?
确实,临界段要比其他的核心态同步对象要快,因为EnterCriticalSection和LeaveCriticalSection这两个函数从InterLockedXXX系列函数中得到不少好处(下面的代码演示了临界段是如何使用InterLockedXXX函数的)。InterLockedXXX系列函数完全运行于用户态空间,根本不需要从用户态到核心态
之间的切换。所以,进入和离开一个临界段一般只需要10个左右的CPU执行指令。而当调用WaitForSingleObject之流的函数时,因为使用了内核对象,线程被强制的在用户态和核心态之间变换。在x86处理器上,这种变换一般需要600个CPU指令。看到这里面的巨大差距了把。
话说回来,临界段是不是真正的“快”?实际上,临界段只在共享资源没有冲突的时候是快的。当一个线程试图进入正在被另外一个线程拥有的临界段,即发生竞争冲突时,临界段还是等价于一个event核心态对象,一样的需要耗时约600个CPU指令。事实上,因为这样的竞争情况相对一般的运行情况来说是很少的(除非人为),所以在大部分的时间里(没有竞争冲突的时候),临界段的使用根本不牵涉内核同步,所以是高速的,只需要10个CPU的指令。(bear说:明白了吧,纯属玩概率,Ms的小花招)
2.3进程边界怎么办?
“临界段等价于一个event核心态对象”是什么意思?
看看临界段结构的定义先
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
#typedef RTL_CRITICAL_SECTION CRITICL_SECTION
在CRITICAL_SECTION 数据结构里,有一个Event内核对象的句柄(那个undocument的结构体成员LockSemaphore,包含的实际是一个event的句柄,而不是一个信号量semaphore)。正如我们所知,内核对象是系统全局的,但是该句柄是进程所有的,而不是系统全局的。所以,就算把一个临界段结构直接放到共享的内存映象里,临界段也无法起作用,因为LockSemaphore里句柄值只对一个进程有效,对于别的进程是没有意义的。在一般的进程同步中,进程要使用一个存在于别的进程里的Event 对象,必须调用OpenEvent或CreaetEvent函数来得到进程可以使用的句柄值。
CRITICAL_SECTION结构里其他的变量是临界段工作所依赖的元素,Ms也“警告”程序员不要自己改动该结构体里变量的值。是怎么实现的呢?看下一步.
2.4 COptex,优化的同步对象类
Jeffrey Richter曾经写过一个自己的临界段,现在,他把他的临界段改良了一下,把它封装成一个COptex类。成员函数TryEnter拥有NT4里介绍的函数TryEnterCriticalSection的功能,这个函数尝试进入临界段,如果失败立刻返回,不会挂起线程,并且支持Spin计数.这个功能在NT4/SP3中被InitializeCriticalSectionAndSpinCount 和SetCriticalSectionSpinCount实现。Spin计数在多处理器系统和高竞争冲突情况下是很有用的,在进入WaitForXXX核心态之前,临界段根据设定的Spin计数进行多次TryEnterCtriticalSection,然后才进行堵塞。想一下,TryEnterCriticalSection才使用10个左右的周期,如果在Spin计数消耗完之前,冲突消失,临界段对象是空闲的,那么再用10个CPU周期就可以在用户态进入临界段了,不用切换到核心态.
(bear说:为了避免这个"核心态",Ms自己也是费劲脑汁呀.看出来了吧,优化的原则:在需要的时候才进入核心态。否则,在用户态进行同步)
以下是COptex代码。原代码下载
Figure 2: COptex
Optex.h
/******************************************************************************
Module name: Optex.h
Written by: Jeffrey Richter
Purpose: Defines the COptex (optimized mutex) synchronization object
******************************************************************************/
#pragma once
///////////////////////////////////////////////////////////////////////////////
class COptex {
public:
COptex(LPCSTR pszName, DWORD dwSpinCount = 4000);
COptex(LPCWSTR pszName, DWORD dwSpinCount = 4000);
~COptex();
void SetSpinCount(DWORD dwSpinCount);
void Enter();
BOOL TryEnter();
void Leave();
private:
typedef struct {
DWORD m_dwSpinCount;
long m_lLockCount;
DWORD m_dwThreadId;
long m_lRecurseCount;
} SHAREDINFO, *PSHAREDINFO;
BOOL m_fUniprocessorHost;
HANDLE m_hevt;
HANDLE m_hfm;
PSHAREDINFO m_pSharedInfo;
private:
BOOL CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount);
};
///////////////////////////////////////////////////////////////////////////////
inline COptex::COptex(LPCSTR pszName, DWORD dwSpinCount) {
CommonConstructor((PVOID) pszName, FALSE, dwSpinCount);
}
///////////////////////////////////////////////////////////////////////////////
inline COptex::COptex(LPCWSTR pszName, DWORD dwSpinCount) {
CommonConstructor((PVOID) pszName, TRUE, dwSpinCount);
}
Optex.cpp
/******************************************************************************
Module name: Optex.cpp
Written by: Jeffrey Richter
Purpose: Implements the COptex (optimized mutex) synchronization object
******************************************************************************/
#include <windows.h>
#include "Optex.h"
///////////////////////////////////////////////////////////////////////////////
BOOL COptex::CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount)
{
m_hevt = m_hfm = NULL;
m_pSharedInfo = NULL;
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
m_fUniprocessorHost = (sinf.dwNumberOfProcessors == 1);
char szNameA[100];
if (fUnicode) { // Convert Unicode name to ANSI
wsprintfA(szNameA, "%S", pszName);
pszName = (PVOID) szNameA;
}
char sz[100];
wsprintfA(sz, "JMR_Optex_Event_%s", pszName);
m_hevt = CreateEventA(NULL, FALSE, FALSE, sz);
if (m_hevt != NULL) {
wsprintfA(sz, "JMR_Optex_MMF_%s", pszName);
m_hfm = CreateFileMappingA(NULL, NULL, PAGE_READWRITE, 0, sizeof(*m_pSharedInfo), sz);
if (m_hfm != NULL) {
m_pSharedInfo = (PSHAREDINFO) MapViewOfFile(m_hfm, FILE_MAP_WRITE,
0, 0, 0);
// Note: SHAREDINFO's m_lLockCount, m_dwThreadId, and m_lRecurseCount
// members need to be initialized to 0. Fortunately, a new pagefile
// MMF sets all of its data to 0 when created. This saves us from
// some thread synchronization work.
if (m_pSharedInfo != NULL)
SetSpinCount(dwSpinCount);
}
}
return((m_hevt != NULL) && (m_hfm != NULL) && (m_pSharedInfo != NULL));
}
///////////////////////////////////////////////////////////////////////////////
COptex::~COptex() {
#ifdef _DEBUG
if (m_pSharedInfo->m_dwThreadId != 0) DebugBreak();
#endif
UnmapViewOfFile(m_pSharedInfo);
CloseHandle(m_hfm);
CloseHandle(m_hevt);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::SetSpinCount(DWORD dwSpinCount) {
if (!m_fUniprocessorHost)
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwSpinCount, dwSpinCount);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::Enter() {
// Spin, trying to get the Optex
if (TryEnter()) return;
DWORD dwThreadId = GetCurrentThreadId(); // The calling thread's ID
if (InterlockedIncrement(&m_pSharedInfo->m_lLockCount) == 1) {
// Optex is unowned, let this thread own it once
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId, dwThreadId);
m_pSharedInfo->m_lRecurseCount = 1;
} else {
// Optex is owned by a thread
if (m_pSharedInfo->m_dwThreadId == dwThreadId) {
// Optex is owned by this thread, own it again
m_pSharedInfo->m_lRecurseCount++;
} else {
// Optex is owned by another thread
// Wait for the Owning thread to release the Optex
WaitForSingleObject(m_hevt, INFINITE);
// We got ownership of the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId,
dwThreadId); // We own it now
m_pSharedInfo->m_lRecurseCount = 1; // We own it once
}
}
}
///////////////////////////////////////////////////////////////////////////////
BOOL COptex::TryEnter() {
DWORD dwThreadId = GetCurrentThreadId(); // The calling thread's ID
// If the lock count is zero, the Optex is unowned and
// this thread can become the owner of it now.
BOOL fThisThreadOwnsTheOptex = FALSE;
DWORD dwSpinCount = m_pSharedInfo->m_dwSpinCount;
do {
fThisThreadOwnsTheOptex = (0 == (DWORD)
InterlockedCompareExchange((PVOID*) &m_pSharedInfo->m_lLockCount,
(PVOID) 1, (PVOID) 0));
if (fThisThreadOwnsTheOptex) {
// We now own the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId,
dwThreadId); // We own it
m_pSharedInfo->m_lRecurseCount = 1; // We own it once
} else {
// Some thread owns the Optex
if (m_pSharedInfo->m_dwThreadId == dwThreadId) {
// We already own the Optex
InterlockedIncrement(&m_pSharedInfo->m_lLockCount);
m_pSharedInfo->m_lRecurseCount++; // We own it again
fThisThreadOwnsTheOptex = TRUE; // Return that we own the Optex
}
}
} while (!fThisThreadOwnsTheOptex && (dwSpinCount-- > 0));
// Return whether or not this thread owns the Optex
return(fThisThreadOwnsTheOptex);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::Leave() {
#ifdef _DEBUG
if (m_pSharedInfo->m_dwThreadId != GetCurrentThreadId())
DebugBreak();
#endif
if (--m_pSharedInfo->m_lRecurseCount > 0) {
// We still own the Optex
InterlockedDecrement(&m_pSharedInfo->m_lLockCount);
} else {
// We don't own the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId, 0);
if (InterlockedDecrement(&m_pSharedInfo->m_lLockCount) > 0) {
// Other threads are waiting, wake one of them
SetEvent(m_hevt);
}
}
}
///////////////////////////////// End of File /////////////////////////////////
使用这个COptex是很简单的事情,只要构造用下面这两种构造函数一个C++类的实例即可.
构造函数
COptex(LPCSTR pszName, DWORD dwSpinCount = 4000);
COptex(LPCWSTR pszName, DWORD dwSpinCount = 4000);
他们都调用了
BOOL CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount);
构造一个COptex对象必须给它一个字符串型的名字,在突破进程边界的时候这是必须的,只有这个名字能提供共享访问.构造函数支持ANSI或Unicode的名字。
当另外一个进程使用相同的名字构造一个COptex对象,构造函数如何发现已经存在的COptex对象?在CommonConstructor代码中用CreateEvent尝试创建一个命名Event对象,如果这个名字的Event对象已经存在,那么,得到该对象的句柄,并且GetLastError可以得到ERROR_ALREADY_EXISTS.如果不存在则创建一个.如果创建失败,则得到的句柄为NULL.
同样的,可以得到一个共享的内存映象文件的句柄.
构造成功后,在需要同步时,根据情况简单的执行相应的进程间同步操作。构造函数的第二个参数用来指定Spin计数,默认是4000(这是操作系统序列化堆Heap的函数所使用的数量.操作系统在分配和释放内存的时候,要序列化进程的堆,这时也要用到临界段)
COptex类的其他函数和Win32函数是一一对应的.熟悉同步对象的程序员应该很容易理解.
COptex是如何工作的呢?实际上,一个COptex包含两个数据块(Data blocks):一个本地的,私有的;另一个是全局的,共享的.一个COptex对象构造之后,本地数据块包含了COptex的成员变量:m_hevt变量初始化为一个命名事件对象句柄;m_hfm变量初始化为一个内存映象文件对象句柄.既然这些句柄代表的对象是命名的,那么,他们可以在进程间共享。注意,是"对象"可以共享,而不是"对象的句柄".每个进程内的COptex对象都必须保持这些句柄在本进程内的值.
m_pShareInf成员指向一个内存映象文件,全局数据块在这个内存映象文件里,以指定的共享名存在. SHAREDINFO结构是内存映象数据的组织方式,该结构在COptex类里定义,和CRITCIAL_SECTION的结构非常相似.
typedef struct {
DWORD m_dwSpinCount;
long m_lLockCount;
DWORD m_dwThreadId;
long m_lRecurseCount;report-2001-03-07.htm
} SHAREDINFO, *PSHAREDINFO;
m_dwSpinCount : spin计数
m_lLockCount : 锁定计数
m_dwThreadID : 拥有该临界段的线程ID
m_lRecurseCount:本线程拥有该临界段的计数
好了,仔细看看代码吧,大师风范呀.注意一下在进行同步时,关于是否同一线程,关于LockCount的值的一系列的判断,以及InterLockedXXX系列函数的使用,具体用法查MSDN.
bear最喜欢这样的代码了,简单明了,思路清晰,原理超值,看完了只想大喝一声"又学一招,爽!"
bear也写累了 ,收工:).
2001.3.2
随意转载,只要不去掉Jeffrey的名字,还有bear的:D
翻译有错,请找vcbear@sina.com或留言,不懂Win32编程看下面:
Have a question about programming in Win32? Contact Jeffrey Richter at http://www.jeffreyrichter.com/
From the January 1998 issue of Microsoft Systems Journal.
原文网址:http://www.vchome.net/tech/multithread/thread23.htm#_Toc536438510
- 多线程编程技术开发资料
- 多线程编程技术开发资料
- 【140817】多线程编程技术开发 pdf
- 多线程游戏服务器技术开发
- 编程资料 -C# 多线程
- 编程资料 -C# 多线程
- 编程资料 -C# 多线程
- 多线程编程最新资料大全
- 多线程编程最新资料大全
- 多线程编程最新资料大全
- Java 5.0多线程编程学习资料笔录
- C# 多线程并发编程资料汇总学习
- Java多线程编程相关资料推荐
- Linux下的多线程编程 学习资料
- 技术开发
- 技术开发
- 多线程资料
- 多线程资料
- 翻回头看自己走过的路
- 再次测试
- 从C#的Singleton设计模式实现看.NET Framework特性对开发者的重要性
- 怎样重设目录恢复模式下的管理员的密码
- 找工作中的一些感悟
- 多线程编程技术开发资料
- 今天是我第一天写Blog,浅谈Blog的好处!
- 人生哲理
- Free Talk : 转载:作为一个合格程序员每天该做的事
- 合格程序员的每天每周每月每年
- 如何解决 " 无法连接到知识库 " 错误信息
- 开创世纪(1)
- Coupling and Cohesion
- 让VB自动改变控件大小