多线程上下文切换
本文来自方腾飞老师《java并发编程的艺术》第一章。
并发编程的目的是为了让程序运行得更快,但是并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题,本文要研究的是上下文切换的问题。
一、cpu时间片
- cpu时间片即cpu分配给每个线程的执行时间段,称作它的时间片。cpu时间片一般为几十毫秒(ms)。
二、什么是上下文切换
cpu通过时间片段的算法来循环执行线程任务,而循环执行即每个线程允许运行的时间后的切换,而这种循环的切换使各个程序从表面上看是同时进行的。而切换时会保存之前的线程任务状态,当切换到该线程任务的时候,会重新加载该线程的任务状态。从任务保存到再加载的过程就是一次上下文切换。
- 这就像我们同时读两本书,当我们在读一本英文的技术书籍时,发现某个单词不认识,于是便打开中英文词典,但是在放下英文书籍之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
三、上下文切换造成的影响
我们可以通过对比串联执行和并发执行进行对比。
1 private static final long count = 1000000; 2 3 public static void main(string[] args) throws exception { 4 concurrency(); 5 series(); 6 } 7 /** 8 * 并发执行 9 * @throws exception 10 */ 11 private static void concurrency() throws exception { 12 long start = system.currenttimemillis(); 13 //创建线程执行a+= 14 thread thread = new thread(new runnable() { 15 public void run() { 16 int a = 0; 17 for (int i = 0; i < count; i++) { 18 a += 1; 19 } 20 } 21 }); 22 //启动线程执行 23 thread.start(); 24 //使用主线程执行b--; 25 int b = 0; 26 for (long i = 0; i < count; i++) { 27 b--; 28 } 29 //合并线程,统计时间 30 thread.join(); 31 long time = system.currenttimemillis() - start; 32 system.out.println("concurrency:" + time + "ms, b = " + b); 33 } 34 /** 35 * 串联执行 36 */ 37 private static void series() { 38 long start = system.currenttimemillis(); 39 int a = 0; 40 for (long i = 0; i < count; i++) { 41 a += 1; 42 } 43 int b = 0; 44 for (int i = 0; i < count; i++) { 45 b--; 46 } 47 long time = system.currenttimemillis() - start; 48 system.out.println("serial:" + time + "ms, b = " + b + ", a = " + a); 49 }
修改上面的count值,即修改循环次数,对比一下串行运行和并发运行的时间测试结果:
循环次数 | 串行执行耗时/ms | 并发执行耗时/ms | 串行和并发对比 |
1亿 | 78 | 50 | 并发快约0.5倍 |
1000万 | 10 | 6 | 并发快约0.5~1倍 |
100万 | 3 | 2 | 差不多 |
10万 | 2 | 2 | 差不多 |
1万 | 0 | 1 | 差不多,十几次执行下来,总体而言串行略快 |
从表中可以看出,100次并发执行累加以下,串行执行和并发执行的运行速度总体而言差不多,1万次以下串行执行甚至还
在linux系统下可以使用vmstat命令来查看上下文切换的次数,如果要查看上下文切换的时长,可以利用lmbench3,这是一个性能分析工具。通过数据的对比我们可以看出。在一万以下的循环次数时,串联的执行速度比并发的执行速度块。是因为线程上下文切换导致额外的开销。
四、引起线程上下文切换的原因
对于我们经常使用的抢占式操作系统而言,引起线程上下文切换的原因大概有以下几种:
- 当前执行任务的时间片用完之后,系统cpu正常调度下一个任务
- 当前执行任务碰到io阻塞,调度器将此任务挂起,继续下一任务
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
- 用户代码挂起当前任务,让出cpu时间
- 硬件中断
五、上下文切换次数查看
在linux系统下可以使用vmstat命令来查看上下文切换的次数,下面是利用vmstat查看上下文切换次数的示例:
cs(context switch)表示上下文切换的次数,从图中可以看到,上下文每秒钟切换500~600次左右。
如果要查看上下文切换的时长,可以利用lmbench3,这是一个性能分析工具。
六、如何减少上下文切换导致额外的开销
减少上下文切换次数便可以提高多线程的运行效率。减少上下文切换的方法有无锁并发编程、cas算法、避免创建过多的线程和使用协程。
-
无锁并发编程. 当任何特定的运算被阻塞的时候,所有cpu可以继续处理其他的运算。换种方式说,在无锁系统中,当给定线程被其他线程阻塞的时候,所有cpu可以不停的继续处理其他工作。无锁算法大大增加系统整体的吞吐量,因为它只偶尔会增加一定的交易延迟。大部分高端数据库系统是基于无锁算法而构造的,以满足不同级别。
-
cas算法。java提供了一套原子性操作的数据类型(java.util.concurrent.atomic包下),使用cas算法来更新数据,不需要加锁。如:atomicinteger、atomiclong等。
-
避免创建过多的线程。如任务量少时,尽可能减少创建线程。对于某个时间段任务量很大的这种情况,我们可以通过线程池来管理线程的数量,避免创建过多线程。
-
协程:即协作式程序,其思想是,一系列互相依赖的协程间依次使用cpu,每次只有一个协程工作,而其他协程处于休眠状态。如:java中使用wait和notify来达到线程之间的协同工作。
参考:
《java并发编程的艺术》
作者:calvin_di
链接:https://www.jianshu.com/p/19fc8aca712c
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。