線程上下文切換的性能損耗測試
線程上下文切換的性能損耗到底有多少,一直沒有直觀的理解,今天寫個程序測試一下。先看看下面的程序(點擊下載):
ThreadTester是所有Tester的基類。所有的Tester都干的是同樣一件事情,把counter增加到100000000,每次只能加1。
1: public abstract class ThreadTester 2: { 3: public const long MAX_COUNTER_NUMBER = 100000000; 4: 5: private long _counter = 0; 6: 7: //獲得計數 8: public virtual long GetCounter() 9: { 10: return this._counter; 11: } 12: 13: //增加計數器 14: protected virtual void IncreaseCounter() 15: { 16: this._counter += 1; 17: } 18: 19: //啟動測試 20: public abstract void Start(); 21: 22: //獲得Counter從開始增加到現在的數字所耗的時間 23: public abstract long GetElapsedMillisecondsOfIncreaseCounter(); 24: 25: //測試是否正在運行 26: public abstract bool IsTesterRunning(); 27: } |
SingleThreadTester是單線程計數。
1: class SingleThreadTester : ThreadTester 2: { 3: private Stopwatch _aStopWatch = new Stopwatch(); 4: 5: public override void Start() 6: { 7: _aStopWatch.Start(); 8: 9: Thread aThread = new Thread(() => WorkInThread()); 10: aThread.Start(); 11: } 12: 13: public override long GetElapsedMillisecondsOfIncreaseCounter() 14: { 15: return this._aStopWatch.ElapsedMilliseconds; 16: } 17: 18: public override bool IsTesterRunning() 19: { 20: return _aStopWatch.IsRunning; 21: } 22: 23: private void 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: public override void 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: public override long GetElapsedMillisecondsOfIncreaseCounter() 18: { 19: return this._aStopWatch.ElapsedMilliseconds; 20: } 21: 22: public override bool IsTesterRunning() 23: { 24: return _aStopWatch.IsRunning; 25: } 26: 27: private void 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: private void 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: private readonly int _threadCount = 0; 5: private readonly object _counterLock = new object(); 6: 7: public MultiThreadTester(int threadCount) 8: { 9: this._threadCount = threadCount; 10: } 11: 12: public override void 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: public override long GetElapsedMillisecondsOfIncreaseCounter() 24: { 25: return this._aStopWatch.ElapsedMilliseconds; 26: } 27: 28: public override bool IsTesterRunning() 29: { 30: return _aStopWatch.IsRunning; 31: } 32: 33: private void 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: static void 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: private static string GetStatusOfThreadTester(ThreadTester tester) 30: { 31: return string.Format("[耗時{0}ms] counter = {1}, {2}", 32: tester.GetElapsedMillisecondsOfIncreaseCounter(), tester.GetCounter(), 33: tester.IsTesterRunning() ? "running" : "stopped"); 34: } 35: 36: private static ThreadTester GreateThreadTesterByInputText(string inputText) 37: { 38: switch (inputText) 39: { 40: case "1": 41: return new SingleThreadTester(); 42: case "2": 43: return new TwoThreadSwitchTester(); 44: default: 45: return new MultiThreadTester(100); 46: } 47: } 48: 49: private static string 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:

計數期間,線程總共切換了846-329=517次。(329是啟動程序后,初始的數值)
Multi Threads - 2 Threads:
計數期間,線程總共切換了295-201=94次。(201是啟動程序后,初始的數值)
從上面收集的數據來看,和我們的判斷基本相符。
干活的其實是CPU,而不是線程
再想想原來學過的知識,之前一直以為線程多干活就快,簡直是把學過的計算機原理都還給老師了。真正干活的不是線程,而是CPU。線程越多,干活不一定越快。
那么高并發的情況下什么時候適合單線程,什么時候適合多線程呢?
適合單線程的場景:單個線程的工作邏輯簡單,而且速度非常快,比如從內存中讀取某個值,或者從Hash表根據key獲得某個value。Redis和Node.js這類程序都是單線程,適合單個線程簡單快速的場景。
適合多線程的場景:單個線程的工作邏輯復雜,等待時間較長或者需要消耗大量系統運算資源,比如需要從多個遠程服務獲得數據并計算,或者圖像處理。
例子程序:http://pan.baidu.com/s/1ntNUPWP
posted on 2014-06-20 11:30 順其自然EVO 閱讀(442) 評論(0) 編輯 收藏 所屬分類: 測試學習專欄 、性能測試