线程上下文切换的性能损耗到底有多少,一直没有直观的理解,今天写个程序测试一下。先看看下面的程序(点击下载):
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
最新内容请见作者的GitHub页:http://qaseven.github.io/