Yesky首页| 产品报价| 行情| 手机 | 数码 | 笔记本 | 台式机 | DIY硬件 | 外设 | 网络 | 数字家庭 | 评测 | 软件 | e时代 | 游戏 | 图片 | 壁纸 | 群乐 | 社区 | 博客 | 下载
您现在的位置: 天极网 > Win32 多线程的性能
全文

Win32 多线程的性能

2002-08-19 10:43 作者: Ruediger R. Asche 出处: 化境编程界 责任编辑:
  快速响应(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 具有相同的参数列表,但是计算是被以串行的顺序处理的,并且以列表中的第一个对象开始。   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{
pTerminator((LPVOID)ObjectArray[iLoop],(LPVOID)pProcessor((LPVOID)ObjectArray[iLoop]));
};
return TRUE;

};

  这段代码只是循环遍历该数组,在每一次迭代中调用处理器,然后在处理器和对象本身的结果上调用终结器。干得既干净又漂亮,不是吗?

  令人感兴趣的成员函数是 DoForAllObjects。乍一看,DoForAllObjects 所要做的也没有什么特别的--请求操作系统创建为每一个计算一个线程,并且确保终结器函数能够被正确地调用。但是,有两个问题使得 DoForAllObjects 比它的表面现象要复杂:第一,当计算的数目多于可用的线程数时,ConcurrentExecution 的一个实例所创建的"并发的最大度数"参数可能需要一些附加的记录(bookkeeping)。第二,每一个计算的终结器函数都是在调用 DoForAllObjects 的线程的上下文中被调用的,而不是在该计算运行所处的线程上下文中被调用的;并且,终结器是在处理器结束之后立刻被调用的。要处理这些问题还是需要很多技巧的。

共6页。 9 1 2 3 4 5 6 :
网友关注
最新上市
编辑推荐
文章阅读排行
周排行
月排行
欢迎订阅天极网RSS聚合资讯:http://www.yesky.com/index.xml