多執行緒為什麼跑的比單執行緒還要慢的情況分析及驗證
“多個人幹活比一個人幹活要快,多執行緒並行執行也比單執行緒要快”這是我學習程式設計長期以來的想法。然而在實際的開發過程中,並不是所有情況下都是這樣。先看看下面的程式(點選下載):
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:publicoverridevoid 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之間得共享資料, 間接消耗對於程式的影響要看執行緒工作區操作資料的大小).
根據上面上下文切換的定義,我們做出下面的假設:
- 之所以TwoThreadSwitchTester執行速度最慢,因為執行緒上下文切換的次數最多,時間主要消耗在上下文切換了,兩個執行緒交替計數,每計數一次就要做一次執行緒切換。
- “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:
計數期間,執行緒