欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

多线程上下文切换

程序员文章站 2022-03-29 10:40:58
本文来自方腾飞老师《Java并发编程的艺术》第一章。 并发编程的目的是为了让程序运行得更快,但是并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题,本 ......

本文来自方腾飞老师《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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。