成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

并行設計:如何高效解決同步互斥問題?

開發 前端
從計算機早期的圖靈機模型,直至面向過程、面向對象的軟件編程模型,軟件工程師向來都傾向于運用串行思維來思考與解決問題。伴隨著多核時代的到來,受限于硬件層面并發技術的進步,為了更充分地發揮 CPU 的價值,必須依靠軟件層的并行設計來進一步提高系統性能。

我曾經負責主導了一個性能優化的項目,此項目的主要業務邏輯在于在線搶貨并購買。在起初的設計方案里,鑒于要保證庫存數據的一致性,后端服務于請求處理時運用了 Redis 互斥鎖,然而這致使系統的吞吐量被限制在 30TPS,無法通過彈性擴展來增強性能。那這個問題是如何解決的呢?后來,我們通過采用無鎖化來達成性能的拓展,系統吞吐量一下子提高到了 1000TPS,相較原來提升了足足 30 倍。

由此可見,同步互斥屬于影響并發系統性能的關鍵要素之一,倘若處理不當,甚至可能引發死鎖或者導致系統崩潰的危險。在這節課中,我將會帶領你去探尋并發系統里存在的同步互斥問題,一同思考、剖析引發這些問題的根源究竟是什么,隨后我還會介紹各種同步互斥手段的內部實現詳情,助力你理解運用同步互斥的具體原理以及解決的思路。如此一來,在你深入領會同步互斥問題的本質模型以后,就能夠更為精確地設計并發系統中的同步互斥策略,進而有助于提升系統的關鍵性能。好啦,接下來,咱們就從并發系統中現存的同步互斥問題著手,一起來瞧瞧引起同步互斥問題的內在根源是什么吧。

并行執行的核心問題

從計算機早期的圖靈機模型,直至面向過程、面向對象的軟件編程模型,軟件工程師向來都傾向于運用串行思維來思考與解決問題。伴隨著多核時代的到來,受限于硬件層面并發技術的進步,為了更充分地發揮 CPU 的價值,必須依靠軟件層的并行設計來進一步提高系統性能。然而,當下大多數軟件工程師依舊習慣以串行思維來處理問題,這便會致使所設計實現的軟件系統不但性能極差,而且容易出現故障。比方說,我們不妨來看看這個并發程序,探尋一下它在執行過程中可能會存在哪些問題。

int number_1 = 0;
int number_2 = 0;
void atom_increase_call()
{
for (int i = 0; i < 10000; i++)
     {
         number_1++;
         number_2++;
     } 
}
void atom_read_call()
{
int inorder_count = 0;
for (int i = 0; i < 10000; i++) 
     {  
if (number_2 > number_1)
         {
             inorder_count++;
         } 
     }
std::cout << "thread:3 read inorder_number is " << inorder_count
             << std::endl;
}
int main()
{
std::thread threadA(atom_increase_call);
std::thread threadB(atom_increase_call);
std::thread threadC(atom_read_call);
     threadA.join();
     threadB.join();
     threadC.join();
std::cout << "thread:main read number is " << number_1 << std::endl;
return 0;
}

運行之后你會發現,由于代碼在三個線程上并行執行,導致這個程序每次的運行結果可能都不相同,這種現象就被叫做程序運行結果不確定性,而這通常是業務所不能接受的。這里我列舉了其中兩次執?結果,如下:

| 第?次:
thread:3 read inorder_number is 1
thread:main read number_1 is 15379
thread:main read number_2 is 15378


| 第?次:
thread:3 read inorder_number is 13
thread:main read number_1 is 15822
thread:main read number_2 is 15821

通過對這段代碼的兩次執行結果加以分析,我們能夠看到該并發程序呈現出了兩種現象:在線程 A 和線程 B 中,number_1++、number_2++ 累計執行了 20000 次,照理說結果應當是 20000 ,但實際運行的結果卻與 20000 存在較大差距。

在線程 A 和線程 B 中,都是先進行 number_1++ 的操作,然后再執行 number_2++ ,所以 inorder_number 的統計按理應當是 0 才合理,然而最終的結果并非 0 。這表明,number_1++ 與 number_2++ 執行結果的生效,在跨線程的情況下順序并非一致。

那么此刻,我們可以先思考一下:為何在現象 1 中,number_1 的值并非 20000 呢?我覺得可能存在兩個原因:number_1 在不同線程間的緩存失效,致使大量寫入操作與預期不符,進而導致與實際值的偏差較大;number_1++ 的操作包含了讀取、修改這兩個階段,期間有可能被中斷,因而不具備原子特性,這樣一來兩個線程中的 number_1++ 操作相互干擾,也就無法確保結果的正確性。而致使 inorder_number 值不為 0 的原因眾多,比如:變量 number_1 和 number_2 在線程間的緩存不一致;由于編譯器的指令重排序優化,導致 number_1++ 和 number_2++ 生成指令的順序被打亂;由于 CPU 級的指令級并發技術,使得 number_1++ 和 number_2++ 并發執行,從而無法保證執行順序。

如此一來,我們將以上所有問題進行匯總梳理之后,實際上能夠發現導致并發系統執行結果不確定性的根源問題主要有三個,分別是原子性破壞問題、緩存一致性問題、順序一致性問題。那么我們應當如何去解決并發系統中存在的這三個根源問題呢?想必您肯定會想到,運用互斥鎖呀!確實,互斥鎖能夠有效地解決上述三個問題。

下面,我們就一起來了解下互斥鎖是如何解決上面描述的三個問題的,同時在此過程中,我們也來看看由于使用了互斥鎖,都會引入什么樣的性能開銷。

圖片圖片

如圖所示,在 Lock 加鎖后進入臨界區之前,以及退出臨界區后并執行 Unlock 之前,這兩個地方都增添了內存屏障指令(不同的 CPU 架構與 OS 上的實現存在一些差異,不過其基本原理是相似的)。

如此一來,在編譯期間通過這兩個內存屏障,實現了以下的功能:對臨界區與非臨界區之間的指令重排序進行了限制;確保在釋放鎖之前,臨界區中的共享數據已經寫入到內存中,以此保障多線程間的緩存一致性。

由于臨界區是互斥訪問的,所以您可以認為臨界區的業務邏輯整體上是原子性且緩存一致的,并且跨線程間數據順序的一致性約束,也統一在臨界區內得以實現。

雖然臨界區間內的代碼是亂序優化執行的,還存在非原子性操作等情況,不過這都不會對程序執行最終結果的確定性造成影響。

另外,從圖中您還能夠看到,當互斥鎖加鎖失敗后,執行線程會進入休眠狀態,一直到互斥鎖資源被釋放,才會被動地等待內核態重新調度來激活。很明顯,線程長時間的休眠會造成業務阻塞,進而影響到軟件系統的性能。

所以,在并發程序中使用互斥鎖時,一個重要的性能優化手段就是減小臨界區的大小,以此來減少線程可能的阻塞時間。比如說,通過刪掉一些非沖突的業務邏輯,來縮短臨界區的執行代碼時間。

不過在這里,請您再思考一個問題:在通過減少臨界區代碼來優化性能的過程中,如果您發現臨界區的執行時間,已經小于線程休眠切換的時間開銷(通常線程休眠切換的開銷大概在 2us 左右,不同機器在性能上會有一定的差別,需要以實際機器的測試結果為準),那您還會選擇互斥鎖這種方式嗎?實際上,這時候您應該考慮更換一種鎖,以減少線程休眠切換所消耗的時間。接下來我要為您介紹的自旋鎖(SpinLock),就能夠幫助達成這個目的。自旋鎖在 Linux 源碼中被廣泛使用,下面我來給您介紹一下它的基本原理與性能表現吧。

自旋鎖的原理與性能

首先,我們還是來了解下自旋鎖的實現原理,看看它的處理邏輯是怎么樣的,如下圖所示:

圖片圖片

對比前面互斥鎖的工作過程示意圖,您能夠發現,自旋鎖和互斥鎖的邏輯差別主要在于:當加鎖失敗時,當前線程不會進入休眠狀態。所以,如果您采用自旋鎖這種實現方式,倘若臨界區執行的開銷較小,那么就能夠獲取等待時間開銷小于線程休眠切換開銷所帶來的額外收益。

在自旋鎖中,臨界區的實現機制和互斥鎖基本相同,所以它也能夠解決前面所提及的并發系統中的三個根源問題。

另外,和互斥鎖一樣,為了進一步提高軟件的性能,您也需要進一步降低線程間的數據依賴。這樣,經過您設計優化之后,當把線程之間的依賴數據減少到只有幾個變量時,執行的開銷可能僅需要幾個指令周期就能完成。但是在這種情況下使用鎖機制,您還需要在每次數據操作的過程中進行加鎖和解鎖,如此一來,額外開銷的占比就會過高,實際上是不太劃算的。

那么既然這樣,還有其他更為高效的解決辦法嗎?當然有!請牢記,鎖只是我們解決問題的方式,而非我們需要解決的問題。

現在讓我們再次回到問題本身,再來強化記憶一下并發系統內的三個本質問題:原子性破壞問題、緩存一致性問題、順序一致性問題。在這里您需要明白,在具體的并發業務場景中,可能并不需要您同時去解決這三個問題。例如在多線程場景下的統計變量,兩個線程同時更新一個變量,在這里根本就不存在順序一致性的問題。因此,您首先需要學會的是辨別并發系統中有待解決的問題,然后再去精確地尋找解決辦法,這才是進一步提升系統性能的關鍵所在。

那么,在實際的業務場景中,最常見的導致并發系統執行結果不確定性的問題,實際上是緩存一致性問題,比如典型的生產者消費者問題。不過在嵌入式系統的業務場景中,C 語言已經通過引入 volatile 變量解決了這個問題。接下來,我們就通過使用 volatile 來解決問題的工作流程,來分析、了解一下 volatile 是怎樣解決同步互斥中存在的問題的。

volatile 的原理與性能

volatile 是一種特殊變量類型,它主要是為了解決并發系統中的緩存一致性問題。定義為 volatile 類型的變量,會被默認為是緩存失效狀態,針對這個變量的讀取、設置操作,都可以通過直接操作內存來實現,從而就規避了緩存一致性問題。在 C/C++ 語言中,volatile 一直在沿用這種方式,但這種實現機制并沒有完全解決并發系統中的原子性破壞和順序一致性的問題。而在 Java 語言中,JVM 會在 volatile 變量的過程中添加內存屏障機制,從而可以部分解決順序一致性的問題。其具體機制如下圖所示:

圖片圖片

圖中,變量 x、y 屬于 volatile 類型變量,初始值分別是 1 和 2,Load 表示的是對內存直接進行讀取操作,而 Store 代表了對內存直接進行寫入操作。在線程 1 內部,當 volatile 變量 y 進行寫入操作時,會在生成的操作指令前面添加寫屏障指令;而線程 2 在執行 volatile 變量 y 的讀取操作時,在生成的代碼指令后面添加了讀屏障指令。

這樣一來,通過寫屏障就對線程 1 的執行過程進行了限制,使得 Store x 與 Store y 的寫操作不能亂序;讀屏障則對線程 2 的執行過程進行了限制,讓 Load y 和 Load x 不能亂序。

因此,對于線程 2 而言,只可能看到線程 1 執行過程中的 3 個時間點的狀態,分別是:State A :初始化狀態,y=2,x =1。State B :x 剛設置完的中間狀態,y=2,x =5。State C :x、y 都設置完的狀態,y=8,x=5。

而要是線程 1 和線程 2 中的任何一方沒有使用內存屏障指令,就有可能致使線程 2 讀到的數據順序不一致,比如獲取到混亂的狀態,y=8,x=2。

實際上,這也是無鎖編程(也就是不使用操作系統中鎖資源的程序,而互斥鎖需要使用操作系統的鎖資源)中的一個典型的問題解決方式。

但在這里,您還需要留意:volatile 并沒有完全達成原子性。比如說,在以下兩種情況下,就不滿足原子性:類似 i++ 這種針對數據的更新操作,在 CPU 層面無法通過一條指令就完成更新,所以使用 volatile 也無法保證原子性;對于 32 位的 CPU 架構來說,64 位的長整型變量的讀取和寫入操作無法在一條指令內完成,所以同樣無法保證原子性。

對于因 32 位與 64 位 CPU 架構之間的差異而導致的原子性問題,我們只能在使用過程中盡量去避開;而針對 i++ 這種更新操作,大部分 CPU 架構都實現了一條特殊的 CPU 指令,來專門解決這個問題。

這個特殊指令就是 CAS 指令,它的實現語義如下:

bool CAS(T* addr, T expected, T newValue)
 {
if( *addr == expected )
     {
          *addr =  newValue;
return true;
     }
else
return false;
 }

該函數所實現的功能為:倘若當前值與 expect 相等,那么就將值更新為 newValue,否則不進行更新;要是更新成功則返回 true,否則返回 false。這條指令是滿足原子性的。

好了,現在我為您總結一下前面的分析過程:在并發系統的同步互斥當中,運用 volatile 能夠實現讀取和寫入操作的原子性,使用 CAS 指令可以實現更新操作的原子性,接著借助內存屏障達成跨線程的順序一致性。在 Java 語言里,正是基于 volatile + CAS + 內存屏障的組合,實現了 Atomic 類型(如果想要更深入地理解 Java 的 Atomic 類型的原理與機制,可以參考閱讀這個文檔),進而支撐解決了并發中的三個本質問題。C++ 在 Atmoic 實現的原理和 Java Atomic 是類似的,不過在 C++ 語言中,它定義了更為豐富的一致性內存模型,可供我們靈活選擇

責任編輯:武曉燕 來源: 二進制跳動
相關推薦

2012-03-09 10:44:11

Java

2024-07-19 09:10:37

2025-02-11 12:29:58

2011-01-14 13:50:37

2009-12-09 09:13:32

Windows 7電影播放

2021-02-03 20:10:29

Linux信號量shell

2025-02-17 02:00:00

Monitor機制代碼

2015-03-04 14:12:58

數據庫mysql工作量

2009-11-28 20:24:13

Linux互斥鎖同步移植

2024-06-28 08:45:58

2010-06-24 09:12:27

.NET 4并行編程

2012-05-17 08:43:26

Windows 7Linux

2015-11-25 11:20:23

WindowsUbuntu時間同步

2021-06-29 11:15:06

云計算云計算環境云應用

2012-09-05 11:09:15

SELinux操作系統

2024-10-18 11:39:55

MySQL數據檢索

2012-04-10 10:04:26

并行編程

2024-09-03 09:08:43

2010-12-12 09:40:00

Android UI設

2018-03-09 16:27:50

數據庫Oracle同步問題
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 在线观看国产视频 | av毛片免费 | 在线观看第一页 | 999精品网| 亚洲精品国产综合区久久久久久久 | 夜色www国产精品资源站 | 成人亚洲片 | 中文无吗 | 久久精品91久久久久久再现 | 福利成人 | 理论片免费在线观看 | 成人免费一区二区三区牛牛 | 黄色一级毛片 | 国产一区二区三区视频 | 亚洲精品一区二区网址 | h视频在线观看免费 | 国产成人免费视频网站高清观看视频 | 免费成人高清在线视频 | 一级大片网站 | 欧美精品tv | 久久美女网 | 亚洲欧美日韩电影 | 久久久久久久国产 | 久久精品屋 | av影片在线 | 国产一伦一伦一伦 | 日本午夜免费福利视频 | 99久久婷婷国产综合精品电影 | 九九热精品视频在线观看 | 欧美日韩在线免费 | 精品一区av | a黄在线观看 | 亚洲激情av | 午夜免费在线观看 | 伊人久久大香线 | 国产成人在线观看免费 | 在线中文字幕亚洲 | 一区二区三区免费在线观看 | 国产精品国产精品国产专区不片 | 欧美精品一区三区 | 亚洲精品在线看 |