📄 csdn_文档中心_win32 多线程的性能(2).htm
字号:
</TD>
<TD align=middle width=620>
<TABLE bgColor=#eeeeee border=0 cellPadding=0 cellSpacing=0 width=600>
<TBODY>
<TR bgColor=#ffffff>
<TD align=middle height=10 width=50></TD>
<TD align=right><A href="http://www.csdn.net/">CSDN</A> - <A
href="http://www.csdn.net/develop/">文档中心</A> - <FONT
color=#003399>Visual C++</FONT> </TD></TR>
<TR>
<TD align=middle height=5></TD>
<TD align=middle width=500></TD></TR>
<TR>
<TD align=middle bgColor=#003399 height=10><FONT
color=#ffffff>标题</FONT></TD>
<TD><B> Win32
多线程的性能(2)</B> vcbear(收藏) </TD></TR>
<TR>
<TD align=middle height=5></TD>
<TD align=middle width=500></TD></TR>
<TR>
<TD align=middle bgColor=#003399><FONT color=#ffffff>关键字</FONT></TD>
<TD width=500> Win32 多线程的性能(2)</TD></TR>
<TR>
<TD align=middle height=5></TD>
<TD align=middle width=500></TD></TR></TBODY></TABLE><!--文章说明信息结束//-->
<TABLE border=0 width=600>
<TBODY>
<TR>
<TD align=left><BR>
<P><FONT color=#ff8000>Win32 多线程的性能(2)</FONT></P>
<TABLE border=0 width="100%">
<TBODY>
<TR>
<TD bgColor=#98a2bc>作者:Microsoft公司供稿</TD></TR></TBODY></TABLE>
<DIV align=left>
<TABLE border=0 width="100%">
<TBODY>
<TR>
<TD bgColor=#ebebeb>Ruediger R. Asche<BR>Microsoft Developer
Network 技术小组<BR>ConcurrentExecution
的内部工作<BR><BR><BR> 请注意:本节的讨论是非常技术性的,所以假设您理解很多有关 Win32 线程 API
的知识。如果您对如何使用 ConcurrentExecution 类来收集测试数据更加感兴趣,而不是对
ConcurrentExecution::DoForAllObjects
是如何被实现的感兴趣,那么您现在就可以跳到下面的“使用 ConcurrentExecution
来试验线程性能”一节。<BR><BR> 让我们从 DoSerial
开始,因为它很大程度上是一个“不费脑筋的家伙”:<BR><BR><BR>BOOL
ConcurrentExecution::DoSerial(int iNoOfObjects,long
*ObjectArray,<BR><BR>CONCURRENT_EXECUTION_ROUTINE
pProcessor,<BR><BR>CONCURRENT_FINISHING_ROUTINE
pTerminator)<BR><BR>{ <BR><BR>for (int
iLoop=0;iLoop<iNoOfObjects;iLoop++)<BR><BR>{<BR><BR>pTerminator((LPVOID)ObjectArray[iLoop],(LPVOID)pProcessor((LPVOID)ObjectArray[iLoop]));<BR><BR>};<BR><BR>return
TRUE;<BR><BR><BR>};<BR><BR><BR> 这段代码只是循环遍历该数组,在每一次迭代中调用处理器,然后在处理器和对象本身的结果上调用终结器。干得既干净又漂亮,不是吗?<BR><BR> 令人感兴趣的成员函数是
DoForAllObjects。乍一看,DoForAllObjects
所要做的也没有什么特别的——请求操作系统创建为每一个计算一个线程,并且确保终结器函数能够被正确地调用。但是,有两个问题使得
DoForAllObjects
比它的表面现象要复杂:第一,当计算的数目多于可用的线程数时,ConcurrentExecution
的一个实例所创建的“并发的最大度数”参数可能需要一些附加的记录(bookkeeping)。第二,每一个计算的终结器函数都是在调用
DoForAllObjects
的线程的上下文中被调用的,而不是在该计算运行所处的线程上下文中被调用的;并且,终结器是在处理器结束之后立刻被调用的。要处理这些问题还是需要很多技巧的。<BR><BR> 让我们深入到代码中,看看究竟是怎么样的。该段代码是从文件
Thrdlib.cpp 中继承来的,但是为了清除起见,已经被精简了:<BR><BR>int
ConcurrentExecution::DoForAllObjects(int iNoOfObjects,long
*ObjectArray,<BR><BR>CONCURRENT_EXECUTION_ROUTINE
pObjectProcessor,<BR><BR>CONCURRENT_FINISHING_ROUTINE<BR><BR>pObjectTerminated)<BR><BR>{<BR><BR>int
iLoop,iEndLoop;<BR><BR>DWORD iThread;<BR><BR>DWORD
iArrayIndex;<BR><BR>DWORD dwReturnCode;<BR><BR>DWORD
iCurrentArrayLength=0;<BR><BR>BOOL
bWeFreedSomething;<BR><BR>char
szBuf[70];<BR><BR><BR>m_iCurrentNumberOfThreads=iNoOfObjects;<BR><BR><BR>HANDLE
*hPnt=(HANDLE
*)VirtualAlloc(NULL,m_iCurrentNumberOfThreads*sizeof(HANDLE)<BR><BR>,MEM_COMMIT,PAGE_READWRITE);<BR><BR>for(iLoop=0;iLoop<m_iCurrentNumberOfThreads;iLoop++)<BR><BR>hPnt[iLoop]
=
CreateThread(NULL,0,pObjectProcessor,(LPVOID)ObjectArray[iLoop],<BR><BR>CREATE_SUSPENDED,(LPDWORD)&iThread);
<BR><BR><BR> 首先,我们为每一个对象创建单独的线程。因为我们使用 CREATE_SUSPENDED
来创建该线程,所以还没有线程被启动。另一种方法是在需要时创建每一个线程。我决定不使用这种替代的策略,因为我发现当在一个同时运行了多个线程的应用程序中调用时,
CreateThread
调用是非常浪费的;这样,同在运行时创建每一个线程相比,在此时创建线程的开销将更加容易接受,<BR><BR><BR>for
(iLoop = 0; iLoop < m_iCurrentNumberOfThreads;
iLoop++)<BR><BR>{<BR><BR>HANDLE
hNewThread;<BR><BR>bWeFreedSomething=FALSE;<BR><BR>//
如果数组为空,分配一个 slot 和 boogie。<BR><BR>if
(!iCurrentArrayLength)<BR><BR>{<BR><BR>iArrayIndex =
0;<BR><BR>iCurrentArrayLength=1;<BR><BR>}<BR><BR>else<BR><BR>{<BR><BR>//
首先,检查我们是否可以重复使用任何的 slot。我们希望在查找一个新的 slot 之前首先//
做这项工作,这样我们就可以立刻调用该就线程的终结器...<BR><BR>iArrayIndex=WaitForMultipleObjects(iCurrentArrayLength,<BR><BR>m_hThreadArray,FALSE,0);<BR><BR>if
(iArrayIndex==WAIT_TIMEOUT) // no slot
free...<BR><BR>{<BR><BR>{<BR><BR>if (iCurrentArrayLength >=
m_iMaxArraySize)<BR><BR>{<BR><BR>iArrayIndex=
WaitForMultipleObjects(iCurrentArrayLength,<BR><BR>m_hThreadArray,FALSE,INFINITE);<BR><BR>bWeFreedSomething=TRUE;<BR><BR>}<BR><BR>else
// 我们可以释放某处的一个
slot,现在就这么做...<BR><BR>{<BR><BR>iCurrentArrayLength++;<BR><BR>iArrayIndex=iCurrentArrayLength-1;<BR><BR>};
// Else iArrayIndex points to a thread that has been
nuked<BR><BR>};<BR><BR>}<BR><BR>else bWeFreedSomething =
TRUE;<BR><BR>}; // 在这里,iArrayIndex
包含一个有效的索引以存储新的线程。<BR><BR>hNewThread =
hPnt[iLoop];<BR><BR>ResumeThread(hNewThread);<BR><BR>if
(bWeFreedSomething)<BR><BR>{
<BR><BR>GetExitCodeThread(m_hThreadArray[iArrayIndex],&dwReturnCode);
//错误<BR><BR>CloseHandle(m_hThreadArray[iArrayIndex]);<BR><BR>pObjectTerminated((void
*)m_hObjectArray[iArrayIndex],(void
*)dwReturnCode);<BR><BR>};<BR><BR>m_hThreadArray[iArrayIndex]
= hNewThread;<BR><BR>m_hObjectArray[iArrayIndex] =
ObjectArray[iLoop];<BR><BR>}; //
循环结束<BR><BR><BR> DoForAllObjects 的核心是 hPnt,它是一个对象数组,这些对象是当
ConcurrentExecution
对象被构造时分配的。该数组能够容纳最大数目的线程,此最大数目与在构造函数中指定的最大并发度数相对应;因此,该数组中的每一个元素都是一个"slot",并有一个计算居于之中。<BR><BR> 关于决定如何填充和释放的
slots 算法如下:该对象数组是从头到尾遍历的,并且对于每一个对象,我们都做如下的事情:如果尚未有 slot
已经被填充,我们使用当前的对象来填充该数组中的第一个 slot,并且继续执行将要处理当前对象的线程。如果至少有一个 slot
被使用,我们使用 WaitForMultipleObjects
函数来决定是否有正在运行的任何计算已经结束;如果是,我们在该对象上调用终结器,并且为新对象“重用”该
slot。请注意,我们也可以首先填充每一个空闲的 slot,直到没有剩余的 slots 为止,然后开始填充空的
slot。但是,如果我们这样做了,那么空出 slot 的终结器函数将不会被调用,直到所有的 slot
都已经被填充,这样就违反了我们有关当处理器结束一个对象时,终结器立刻被调用的要求。<BR><BR> 最后,还有没有空闲
slot 的情况(就是说,当前激活的线程数等于 ConcurrentExecution
对象所允许的最大并发度数)。在这种情况下,WaitForMultipleObjects 将被再次调用以使得
DoForAllObjects 处于“睡眠”状态,直到有一个 slot 空出;只要这种情况一发生,终结器就被在空出 slot
的对象上调用,并且工作于当前对象的线程被继续执行。<BR><BR> 终于,所有的计算要么都已经结束,要么将占有对象数组中的
slot。下列的代码将会处理所有剩余的线程:<BR><BR><BR>iEndLoop =
iCurrentArrayLength;<BR><BR>for
(iLoop=iEndLoop;iLoop>0;iLoop--)<BR><BR>{<BR><BR>iArrayIndex=WaitForMultipleObjects(iLoop,
m_hThreadArray,FALSE,INFINITE);<BR><BR>if
(iArrayIndex==WAIT_FAILED)<BR><BR>{<BR><BR>GetLastError();<BR><BR>_asm
int 3; //
这里要做一些聪明的事...<BR><BR>};<BR><BR>GetExitCodeThread(m_hThreadArray[iArrayIndex],&dwReturnCode);
// 错误?<BR><BR>if
(!CloseHandle(m_hThreadArray[iArrayIndex]))<BR><BR>MessageBox(GetFocus(),"Can't
delete thread!","",MB_OK); //
使它更好...<BR><BR><BR>pObjectTerminated((void
*)m_hObjectArray[iArrayIndex],<BR><BR>(void
*)dwReturnCode);<BR><BR>if (iArrayIndex==iLoop-1) continue; //
这里很好,没有需要向后填充<BR><BR>m_hThreadArray[iArrayIndex]=m_hThreadArray[iLoop-1];<BR><BR>m_hObjectArray[iArrayIndex]=m_hObjectArray[iLoop-1];<BR><BR>};<BR><BR><BR>最后,清除:<BR><BR><BR>if
(hPnt)
VirtualFree(hPnt,m_iCurrentNumberOfThreads*sizeof(HANDLE),<BR><BR>MEM_DECOMMIT);<BR><BR><BR>return
iCurrentArrayLength;<BR><BR><BR>};<BR><BR><BR>使用
ConcurrentExecution 来试验线程性能<BR><BR><BR> 性能测试的范围如下:测试应用程序
Threadlibtest.exe 的用户可以指定是否测试基于 CPU 的或基于 I/O
的计算、执行多少个计算、计算的时间有多长、计算是如何排序的(为了测试最糟的情况与随机延迟),以及计算是被并发执行还是串行执行。<BR><BR> 为了消除意外的结果,每一个测试可以被执行十次,然后将十次的结果拿来平均,以产生一个更加可信的结果。<BR><BR> 通过选择菜单选项
"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
标识符的值即可。<BR><BR> 请牢记,运行整个的测试程序将总是以最糟的情况来排序计算(就是说,执行的顺序是串行的,最长的计算将被首先执行,其后跟随着第二长的计算,然后以次类推)。这种方案牺牲了串行执行的灵活性,因为并发执行的响应时间在一个非最糟的方案下也没有改变,而该串行执行的响应时间是有可能提高的。<BR><BR> 正如我前面所提到的,在一个实际的方案中,您应该分析每一个计算的时间是否是可以预测的。<BR><BR> 使用
ConcurrentExecution 类来收集性能数据的代码位于 Threadlibtestview.cpp
中。示例应用程序本身 (Threadlibtest.exe) 是一个真正的单文档界面 (SDI) 的 MFC
应用程序。所有与示例有关的代码都驻留在 view 类的实现 CThreadLibTestView 中,它是从
CEasyOutputView 继承而来的。(有关对该类的讨论,请参考"Windows NT Security in
Theory and
Practice"。)这里并不包含该类中所有的有趣代码,所包含的大部分是其数字统计部分和用户界面处理部分。执行测试中的
"meat" 在 CThreadLibTestView::ExecuteTest 中,将执行一个测试运行周期。下面是有关
CThreadLibTestView::ExecuteTest 的简略代码:<BR><BR><BR>void
CThreadlibtestView::ExecuteTest()<BR><BR>{<BR><BR>ConcurrentExecution
*ce;<BR><BR>bCPUBound=((m_iCompType&CT_IOBOUND)==0); //
全局...<BR><BR>ce = new ConcurrentExecution(25);<BR><BR>if
(!QueryPerformanceCounter(&m_liOldVal)) return; // 获得当前时间。
<BR><BR>if (!m_iCompType&CT_IOBOUND)
timeBeginPeriod(1);<BR><BR>if
(m_iCompType&CT_CONCURRENT)<BR><BR>m_iThreadsUsed=ce->DoForAllObjects(m_iNumberOfThreads,<BR><BR>(long
*)m_iNumbers,<BR><BR>(CONCURRENT_EXECUTION_ROUTINE)pProcessor,<BR><BR>(CONCURRENT_FINISHING_ROUTINE)pTerminator);<BR><BR>else<BR><BR>ce->DoSerial(m_iNumberOfThreads,<BR><BR>(long
*)m_iNumbers,<BR><BR>(CONCURRENT_EXECUTION_ROUTINE)pProcessor,<BR><BR>(CONCURRENT_FINISHING_ROUTINE)pTerminator);<BR><BR>if
(!m_iCompType&CT_IOBOUND)
timeEndPeriod(1);<BR><BR>delete(ce);<BR><BR><
其他的代码在一个数组中排序结果,以供 Excel
处理...><BR><BR>}<BR><BR><BR> 该段代码首先创建一个 ConcurrentExecution
类的对象,然后,取样当前时间,(用于统计计算所消耗的时间和响应时间),并且,根据所请求的是串行执行还是并发执行,分别调用
ConcurrentExecution 对象 DoSerial 或 DoForAllObjects
成员。请注意,对于当前的执行我请求最大并发度数为 25;如果您想要运行有多于 25
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -