多线程上下文切换

2018-11-29 09:46:44来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

本文来自方腾飞老师《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,这是一个性能分析工具。通过数据的对比我们可以看出。在一万以下的循环次数时,串联的执行速度比并发的执行速度块。是因为线程上下文切换导致额外的开销

四、引起线程上下文切换的原因

对于我们经常使用的抢占式操作系统而言,引起线程上下文切换的原因大概有以下几种:

  1. 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务
  2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务
  3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
  4. 用户代码挂起当前任务,让出CPU时间
  5. 硬件中断

 

五、上下文切换次数查看

在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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Elasticsearch Mapping

下一篇:Zipkin分布式跟踪系统介绍