問題概述
單核CPU的計算機上, 多線程能夠提高程序運行的性能嗎? 這個問題看起來簡單,實際很復雜,設計到多方面的因素. 首先我們要把概念搞清楚, 那就是什么是性能? 一般來說, 我們把運行一個任務所花的時間來評價性能, 所花的時間可以是在CPU上, 也可能是在I/O操作上, 運行任務的程序, 也可能同時在運行另外若干的任務(吞吐量). 這里我們把概念給縮小一下:
我們這里把性能限制在一個程序運行一個任務, 這個任務是只消耗CPU資源(CPU bound), 所花的時間越小, 說明性能越好. 為了純粹地說明問題, 我們排除了數據共享問題, 即線程之間不做任何同步動作, 完全隔離.
從理論上說,如果計算機只執行這一個測試程序, 那么單線程要比多線程性能好,因為多線程需要做線程上下文環境的切換; 而當計算機同時運行其他的進程, 假設其他進程里也有多個大量消耗CPU的任務, 那么我們的程序由於是多線程, 搶到CPU時間片的機會增多, 它的性能應該好於單線程.
理論上是這么回事, 但我們知道, 實踐與理論是有差距的, 我們的測試不可能在真空環境中. 操作系統的實現有高度的戲劇性, 誰也不能預測實際的測試一定與理論相符, 另外在我們實際的運行環境中,各種情況導致的系統差異很大, 因為我們必須做一些測試.
MSDN上有一篇著名的文章<<Win32 Multithreading Performance>>, 里面講的東西與我們很相近.它主要論述的是串行計算和並行計算的性能比較, 我們直接拿它的例子, 進行一些更改, 來測試我們的假設.
首先討論我們測試的主題與它的不同, 主要有2點: 1, 我們討論的任務是一個固定的任務, 而它里面討論的是數個計算量不同的任務, 而計算不同的任務涉及到吞吐量的概念; 2, 我們討論的是線程數量之間的比較(分別測試不同數量的線程), 而它只有單線程與多線程的比較.
圖1: 多個不同的任務並行處理
圖案2: 一個固定的任務並行處理
測試程序介紹
一個任務在這里簡化為一個持續占用CPU的計算, 為了測試的准確性, 中間不可以有任何的I/O操作, 例如:
for (int iCounter=0; iCounter<iLoopCount; iCounter++);
給定的任務量iDelay,單位是毫秒, 有一段模擬的代碼, 從而把一秒的時間轉換為循環數iLoopCount.里面所用的兩個API是QueryPerformanceFrequency和QueryPerformanceCounter.
我們增加一個方法來取代OnWorstcase:
void CThreadlibtestView::OnFixTask()
{
if(!m_iNumberOfThreads)
{
MessageBeep(-1);
return;
};
for (int iLoop=0;iLoop<m_iNumberOfThreads;iLoop++)
{
m_iNumbers[iLoop]=(int)&m_tbConc[iLoop];
m_tbConc[iLoop].iId=iLoop;
if(iLoop==0)
m_tbConc[iLoop].iDelay=m_iDelay/m_iNumberOfThreads+m_iDelay%m_iNumberOfThreads;
else
m_tbConc[iLoop].iDelay=m_iDelay/m_iNumberOfThreads;
m_tbConc[iLoop].tbOutputTarget=this;
m_tbConc[iLoop].iStartOrder=0;
m_tbConc[iLoop].iEndOrder=0;
m_tbConc[iLoop].iTouchCount=0;
};
}
另外我們指定特定的任務量和線程數量:
int iTaskSize[]={100,500,1000,2000,4000};
int iThreadSize[]={2,5,10,15,20};
單線程時候我們調用OnSerial(), 多線程時調用OnConcurrent(), 線程數量分別取iThreadSize里的值.
測試次數仍然保持10次, 取平均值, 整個測試我們分別運行兩次, 第一次在負載很輕的計算機上運行, 第二次在同樣的計算機上加載一個大負載的進程, 此進程里有十個線程, 每個線程都是基於CPU的密集計算, 程序如下:
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
DWORD busyTime=10;
while(true)
{
DWORD startTime=GetTickCount();
for(;GetTickCount()-startTime<=busyTime;)
;
Sleep(1);
}
return 0;
}
特別要注意的是, 兩次計算不能各啟動一個進程, 必須在一個進程中, 因為在程序開始,我們會算出1秒對應循環數, 而每次啟動程序這個數字是不同的, 為了更有意義的比較,我們要求這個數字一定相同.
測試環境: CPU Intel Pentium M 1.7 G; 1047472KB RAM, Windows 2000 professional SP4.
一秒的循環數:146278208
測試結果
圖3: 在負載輕的系統上測試結果
圖4: 在負載重的系統上測試結果
結論和建議
根據測試結果,我們可以得出一些結論(針對單核CPU):
在負載輕的系統上, 多線程不適合處理基於CPU的任務,而在負載重的系統上,多線程可以幫助提高性能.
這是一個模糊和慎重的結論, 因為測試結果里的一些現象我也給不出合適的理由, 應該屬於操作系統”戲劇化”和”不確定性”的表現吧(操作系統會對某些線程動態提高優先級,但本例中的線程優先級保持不變.), 例如在負載輕的機器上, 20個線程和2個線程運行時間差異不大; 在負載重的機器上, 2個線程運行時間比單線程還長, 而當線程數量增加到5時, 性能才有顯著的提高.
所以我的建議是,在單核CPU機器上,處理CPU密集的任務時, 不推薦使用多線程, 除非你對目標機器非常的了解和確定,並經過嚴格的測試. 當然, 涉及到其他I/O操作的任務, 比如等待用戶按鍵, 讀取文件, 網絡通訊等, 多線程才是正當其選的解決方案.