1. 程式人生 > >多執行緒為什麼跑的比單執行緒還要慢的情況分析及驗證

多執行緒為什麼跑的比單執行緒還要慢的情況分析及驗證

“多個人幹活比一個人幹活要快,多執行緒並行執行也比單執行緒要快”這是我學習程式設計長期以來的想法。然而在實際的開發過程中,並不是所有情況下都是這樣。先看看下面的程式(點選下載):

ThreadTester是所有Tester的基類。所有的Tester都乾的是同樣一件事情,把counter增加到100000000,每次只能加1。

   1:publicabstractclass ThreadTester
   2:     {
   3:publicconstlong MAX_COUNTER_NUMBER = 100000000;
   4:  
   5:privatelong _counter = 0;
   6:  
   7:
//獲得計數
   8:publicvirtuallong GetCounter()
   9:         {
  10:returnthis._counter;
  11:         }
  12:  
  13://增加計數器
  14:protectedvirtualvoid IncreaseCounter()
  15:         {
  16:this._counter += 1;
  17:         }
  18:  
  19://啟動測試
  20:publicabstractvoid Start();
  21:  
  22://獲得Counter從開始增加到現在的數字所耗的時間
  23:publicabstractlong GetElapsedMillisecondsOfIncreaseCounter();
  24:  
  25://測試是否正在執行
  26:publicabstractbool IsTesterRunning();
  27:     }

SingleThreadTester是單執行緒計數。

   1:class SingleThreadTester : ThreadTester
   2:     {
   3:private Stopwatch _aStopWatch = new Stopwatch();
   4:  
   5:public
overridevoid Start()
   6:         {
   7:             _aStopWatch.Start();
   8:  
   9:             Thread aThread = new Thread(() => WorkInThread());
  10:             aThread.Start();
  11:         }
  12:  
  13:publicoverridelong GetElapsedMillisecondsOfIncreaseCounter()
  14:         {
  15:returnthis._aStopWatch.ElapsedMilliseconds;
  16:         }
  17:  
  18:publicoverridebool IsTesterRunning()
  19:         {
  20:return _aStopWatch.IsRunning;
  21:         }
  22:  
  23:privatevoid WorkInThread()
  24:         {
  25:while (true)
  26:             {
  27:if (this.GetCounter() > ThreadTester.MAX_COUNTER_NUMBER)
  28:                 {
  29:                     _aStopWatch.Stop();
  30:break;
  31:                 }
  32:  
  33:this.IncreaseCounter();
  34:             }
  35:         }
  36:     }

TwoThreadSwitchTester是兩個執行緒交替計數。

   1:class TwoThreadSwitchTester : ThreadTester
   2:     {
   3:private Stopwatch _aStopWatch = new Stopwatch();
   4:private AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
   5:  
   6:publicoverridevoid Start()
   7:         {
   8:             _aStopWatch.Start();
   9:  
  10:             Thread aThread1 = new Thread(() => Work1InThread());
  11:             aThread1.Start();
  12:  
  13:             Thread aThread2 = new Thread(() => Work2InThread());
  14:             aThread2.Start();
  15:         }
  16:  
  17:publicoverridelong GetElapsedMillisecondsOfIncreaseCounter()
  18:         {
  19:returnthis._aStopWatch.ElapsedMilliseconds;
  20:         }
  21:  
  22:publicoverridebool IsTesterRunning()
  23:         {
  24:return _aStopWatch.IsRunning;
  25:         }
  26:  
  27:privatevoid Work1InThread()
  28:         {
  29:while (true)
  30:             {
  31:                 _autoResetEvent.WaitOne();
  32:
  33:this.IncreaseCounter();
  34:  
  35:if (this.GetCounter() > ThreadTester.MAX_COUNTER_NUMBER)
  36:                 {
  37:                     _aStopWatch.Stop();
  38:break;
  39:                 }
  40:  
  41:                 _autoResetEvent.Set();
  42:             }
  43:         }
  44:  
  45:privatevoid Work2InThread()
  46:         {
  47:while (true)
  48:             {
  49:                 _autoResetEvent.Set();
  50:                 _autoResetEvent.WaitOne();
  51:this.IncreaseCounter();
  52:  
  53:if (this.GetCounter() > ThreadTester.MAX_COUNTER_NUMBER)
  54:                 {
  55:                     _aStopWatch.Stop();
  56:break;
  57:                 }
  58:             }
  59:         }
  60:     }

MultiThreadTester可以指定執行緒數,多個執行緒爭搶計數。

   1:class MultiThreadTester : ThreadTester
   2:     {
   3:private Stopwatch _aStopWatch = new Stopwatch();
   4:privatereadonlyint _threadCount = 0;
   5:privatereadonlyobject _counterLock = newobject();
   6:
   7:public MultiThreadTester(int threadCount)
   8:         {
   9:this._threadCount = threadCount;
  10:         }
  11:  
  12:publicoverridevoid Start()
  13:         {
  14:             _aStopWatch.Start();
  15:  
  16:for (int i = 0; i < _threadCount; i++)
  17:             {
  18:                 Thread aThread = new Thread(() => WorkInThread());
  19:                 aThread.Start();
  20:             }
  21:         }
  22:  
  23:publicoverridelong GetElapsedMillisecondsOfIncreaseCounter()
  24:         {
  25:returnthis._aStopWatch.ElapsedMilliseconds;
  26:         }
  27:  
  28:publicoverridebool IsTesterRunning()
  29:         {
  30:return _aStopWatch.IsRunning;
  31:         }
  32:  
  33:privatevoid WorkInThread()
  34:         {
  35:while (true)
  36:             {
  37:lock (_counterLock)
  38:                 {
  39:if (this.GetCounter() > ThreadTester.MAX_COUNTER_NUMBER)
  40:                     {
  41:                         _aStopWatch.Stop();
  42:break;
  43:                     }
  44:  
  45:this.IncreaseCounter();
  46:                 }
  47:             }
  48:         }
  49:     }

Program的Main函式中,根據使用者的選擇來決定執行哪個測試類。

   1:class Program
   2:     {
   3:staticvoid Main(string[] args)
   4:         {
   5:  
   6:string inputText = GetUserChoice();
   7:  
   8:while (!"4".Equals(inputText))
   9:             {
  10:                 ThreadTester tester = GreateThreadTesterByInputText(inputText);
  11:                 tester.Start();
  12:  
  13:while (true)
  14:                 {
  15:                     Console.WriteLine(GetStatusOfThreadTester(tester));
  16:if (!tester.IsTesterRunning())
  17:                     {
  18:break;
  19:                     }
  20:                     Thread.Sleep(100);
  21:                 }
  22:  
  23:                 inputText = GetUserChoice();
  24:             }
  25:  
  26:             Console.Write("Click enter to exit...");
  27:         }
  28:  
  29:privatestaticstring GetStatusOfThreadTester(ThreadTester tester)
  30:         {
  31:returnstring.Format("[耗時{0}ms] counter = {1}, {2}",
  32:                     tester.GetElapsedMillisecondsOfIncreaseCounter(), tester.GetCounter(),
  33:                     tester.IsTesterRunning() ? "running" : "stopped");
  34:         }
  35:  
  36:privatestatic ThreadTester GreateThreadTesterByInputText(string inputText)
  37:         {
  38:switch (inputText)
  39:             {
  40:case"1":
  41:returnnew SingleThreadTester();
  42:case"2":
  43:returnnew TwoThreadSwitchTester();
  44:default:
  45:returnnew MultiThreadTester(100);
  46:             }
  47:         }
  48:  
  49:privatestaticstring GetUserChoice()
  50:         {
  51:             Console.WriteLine(@"==Please select the option in the following list:==
  52: 1. SingleThreadTester
  53: 2. TwoThreadSwitchTester
  54: 3. MultiThreadTester
  55: 4. Exit");
  56:  
  57:string inputText = Console.ReadLine();
  58:  
  59:return inputText;
  60:         }
  61:     }

三個測試類,執行結果如下:

Single Thread:
[耗時407ms] counter = 100000001, stopped
[耗時453ms] counter = 100000001, stopped
[耗時412ms] counter = 100000001, stopped

Two Thread Switch:
[耗時161503ms] counter = 100000001, stopped
[耗時164508ms] counter = 100000001, stopped
[耗時164201ms] counter = 100000001, stopped

Multi Threads - 100 Threads:
[耗時3659ms] counter = 100000001, stopped
[耗時3950ms] counter = 100000001, stopped
[耗時3720ms] counter = 100000001, stopped

Multi Threads - 2 Threads:
[耗時3078ms] counter = 100000001, stopped
[耗時3160ms] counter = 100000001, stopped
[耗時3106ms] counter = 100000001, stopped

什麼是執行緒上下文切換

上下文切換的精確定義可以參考: http://www.linfo.org/context_switch.html。多工系統往往需要同時執行多道作業。作業數往往大於機器的CPU數,然而一顆CPU同時只能執行一項任務,為了讓使用者感覺這些任務正在同時進行,作業系統的設計者巧妙地利用了時間片輪轉的方式,CPU給每個任務都服務一定的時間,然後把當前任務的狀態儲存下來,在載入下一任務的狀態後,繼續服務下一任務。任務的狀態儲存及再載入,這段過程就叫做上下文切換。時間片輪轉的方式使多個任務在同一顆CPU上執行變成了可能,但同時也帶來了儲存現場和載入現場的直接消耗。(Note. 更精確地說, 上下文切換會帶來直接和間接兩種因素影響程式效能的消耗. 直接消耗包括: CPU暫存器需要儲存和載入, 系統排程器的程式碼需要執行, TLB例項需要重新載入, CPU 的pipeline需要刷掉; 間接消耗指的是多核的cache之間得共享資料, 間接消耗對於程式的影響要看執行緒工作區操作資料的大小).

根據上面上下文切換的定義,我們做出下面的假設:

  1. 之所以TwoThreadSwitchTester執行速度最慢,因為執行緒上下文切換的次數最多,時間主要消耗在上下文切換了,兩個執行緒交替計數,每計數一次就要做一次執行緒切換。
  2. “Multi Threads - 100 Threads”比“Multi Threads - 2 Threads”開的執行緒數量要多,導致執行緒切換次數也比後者多,執行時間也比後者長。

由於Windows下沒有像Linux下的vmstat這樣的工具,這裡我們使用Process Explorer看看程式執行的時候執行緒上線文切換的次數。

Single Thread:

計數期間,執行緒總共切換了580-548=32次。(548是啟動程式後,初始的數值)

Two Thread Switch:

計數期間,執行緒總共切換了33673295-124=33673171次。(124是啟動程式後,初始的數值)

Multi Threads - 100 Threads:

計數期間,執行緒