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的操作的性能。 |