多線程上下文切換優化與注意


前言

本文來自方騰飛老師《Java並發編程的藝術》第一章。

並發編程的目的是為了讓程序運行得更快,但是並不是啟動更多的線程就能讓程序最大限度地並發執行。在進行並發編程時,如果希望通過多線程執行任務讓程序運行得更快,會面臨非常多的挑戰,比如上下文切換的問題、死鎖的問題,以及受限於硬件和軟件的資源限制問題,本文要研究的是上下文切換的問題。

 

什么是上下文切換

即使是單核CPU也支持多線程執行代碼,CPU通過給每個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,因為時間片非常短,所以CPU通過不停地切換線程執行,讓我們感覺多個線程時同時執行的,時間片一般是幾十毫秒(ms)。

CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片后會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再次加載這個任務的狀態,從任務保存到再加載的過程就是一次上下文切換

這就像我們同時讀兩本書,當我們在讀一本英文的技術書籍時,發現某個單詞不認識,於是便打開中英文詞典,但是在放下英文書籍之前,大腦必須先記住這本書讀到了多少頁的第多少行,等查完單詞之后,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。

 

上下文切換代碼測試

下面的代碼演示串行和兵法執行並累加操作的時間:

 1 public class ContextSwitchTest
 2 {
 3     private static final long count = 10000;
 4
 5     public static void main(String[] args) throws Exception
 6     {
 7         concurrency();
 8         serial();
 9     }
10
11     private static void concurrency() throws Exception
12     {
13         long start = System.currentTimeMillis();
14         Thread thread = new Thread(new Runnable(){
15             public void run()
16             {
17                 int a = 0;
18                 for (int i = 0; i < count; i++)
19                 {
20                     a += 5;
21                 }
22             }
23         });
24         thread.start();
25         int b = 0;
26         for (long i = 0; i < count; i++)
27         {
28             b --;
29         }
30         thread.join();
31         long time = System.currentTimeMillis() - start;
32         System.out.println("Concurrency:" + time + "ms, b = " + b);
33     }
34
35     private static void serial()
36     {
37         long start = System.currentTimeMillis();
38         int a = 0;
39         for (long i = 0; i < count; i++)
40         {
41             a += 5;
42         }
43         int b = 0;
44         for (int i = 0; i < count; i++)
45         {
46             b --;
47         }
48         long time = System.currentTimeMillis() - start;
49         System.out.println("Serial:" + time + "ms, b = " + b + ", a = " + a);
50     }
51 }

修改上面的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命令來查看上下文切換的次數,下面是利用vmstat查看上下文切換次數的示例:

CS(Context Switch)表示上下文切換的次數,從圖中可以看到,上下文每秒鍾切換500~600次左右。

如果要查看上下文切換的時長,可以利用Lmbench3,這是一個性能分析工具。

 

如何減少上下文切換

既然上下文切換會導致額外的開銷,因此減少上下文切換次數便可以提高多線程程序的運行效率。減少上下文切換的方法有無鎖並發編程、CAS算法、使用最少線程和使用協程。

  • 無鎖並發編程。多線程競爭時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash取模分段,不同的線程處理不同段的數據
  • CAS算法。Java的Atomic包使用CAS算法來更新數據,而不需要加鎖
  • 使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態
  • 協程。在單線程里實現多任務的調度,並在單線程里維持多個任務間的切換

 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM