【并发那些事】可见性问题的万恶之源
【并發(fā)那些事】可見性問題的萬惡之源
硬件工程師為均衡 CPU 與 緩存之間的速度差異,特意加的 CPU 緩存,竟然在多核的場景下陰差陽錯的成為了并發(fā)可見性問題的萬惡之源!(本文過長,如果不是特別無聊,看到這里就可以了)
前言
還記得那些年,你寫的那些多線程 BUG 嗎?明明只想得到個 1 + 1 = 2 的預期,結果他有時候得到 1,有時候得到 3,但偏偏有時候他也會返回正確的 2。明明在本地運行的好好的,一上線一堆詭異的 BUG。你一遍一遍的檢查代碼,一行一行 debug,結果無功而返。
變量為何突然變異?代碼為何亂序運行?條件為何形同虛設?歡迎收看今天的《走進科學》之半夜。。。哦,不對,歡迎閱讀今天的《并發(fā)那些事》之可見性問題的萬惡之源。就像上面說的,我們在寫并發(fā)程序時,經(jīng)常會出現(xiàn)超出我們認識與直覺的問題,而按我們的以往的經(jīng)驗,很難去察覺到他的問題所在。而又因為我們不了解他發(fā)生的誘因,即使我們按照書上的方案解決了,但是下次還是會出現(xiàn)。所以本文的主旨并不是解決問題的術,而是解決問題的道。一起來探究多線程問題的根源。
首先揭開謎底,大多數(shù)并發(fā)問題的發(fā)生都是這三個問題導致的,可見性問題、原子性問題、有序性問題。那么又是什么導致這三個問題的出現(xiàn)呢?本文將一步步解析可見性問題出現(xiàn)的原因。
核心矛盾
眾所周知,電腦由很多的部件組成。其中最最最重要的有三個,它們分別是 CPU 、內存、IO(硬盤)。一般來說它們三個的性能高低直接影響到了電腦的整體的性能優(yōu)劣。
但是從它們誕生之初,就有一個核心矛盾,即使過了幾十年后的現(xiàn)在,科技的飛速發(fā)展依舊沒能解決。那么是什么矛盾呢?
在說矛盾之前,先說我個同事,他是個電競高手,英雄聯(lián)盟、王者榮耀什么的意識特別厲害。每次看比賽的時候那種指點江山、揮斥方遒的英姿閃閃發(fā)光。但是呢,一上手打游戲,一頓操作猛如虎,一看戰(zhàn)績 0 杠 5,剛開始我們以為他是個青銅,但是呢,很多時候游戲的真的就像他說的那樣,他的預判,他的操作其實都相當?shù)娘L騷。一直很疑惑,直到我們得出了一個結論,其實他的確是一個王者,因為他滿腦子都是騷操作,但是呢?他的雙手跟不上他風騷的大腦。
問題就在這里,核心矛盾就是速度的差異。CPU 就像是那位同事的大腦,很強很風騷,但是奈何 IO 就像那雙跟不上節(jié)奏的手,限制了發(fā)揮。而且它們之間的速度差異要遠遠超出我們的想像,CPU 就好比是火箭,那么內存就是三輪車,IO 可能就是馬路旁一只不起眼的小蝸牛。
各方的努力
既然有了這個問題,那就要想辦法解決,首先這個問題出在硬件層,所以首當其沖的硬件工作師想了很多方式試圖去解決。經(jīng)過內存跟 IO 硬件工程師的不懈努力,這兩個組件的速度都得到了大幅提升。但是呢?CPU 的工程師也沒閑著,甚至英特爾的 CEO--高登·摩爾還宣布了一個以自己姓名定義的摩爾定律[1]。其內容大致如下:
集成電路上可容納的晶體管數(shù)目,約每 18 個月便會增加一倍
可以簡單的理解,CPU 每 18 個月性能就能翻一倍。這就讓內存跟 IO 的硬件工程師很絕望了,不怕別人比你聰明,就怕比你聰明的人還比你努力。這還是怎么玩?
當然,獨木不成林,CPU 工程師也意識到了這個問題,我再怎么獨領風騷,以 1V5。沒有用呀?打的正嗨,一回頭,家被推了。我下了一部電影,雙擊打開,CPU 飛速運行,IO 在緩慢加載。我 CPU 運行到冒煙也沒用呀,IO 制約了。結果就是電影變成了 PPT,一秒一停。這樣下去大家都沒得玩。眼看其它隊友帶不動,CPU 工程師想出了一個辦法,我在 CPU 里面劃一塊出來做為緩存,這個緩存介于 CPU 與 內存之間,跟我們常用的緩存功能差不多,為了均衡 CPU 與內存之間的速度差,在執(zhí)行的時候會把數(shù)據(jù)先從 IO 加載到 內存,再把內存中的數(shù)據(jù)加載到 CPU 的緩存之中。將常用或者將用的數(shù)據(jù)緩存在 CPU 中后,CPU 每次處理時就不用老是等內存了,這極大的提高了 CPU 的利用率。
到這里,硬件工程師圓滿的完成了任務,下面輪到了我們軟件工程師登場了。
雖然說加了緩存之后,CPU 的利用率成倍上升,從當初的運行 5 分鐘,加載 2 小時。變成了,運行 2 分鐘,加載 1 小時,但是體驗還是很差。還拿電影舉例,看電影的時候不光有畫面,還得有聲音呀,你運行是快了,但是先放視頻,再放聲音。就像是先看一部默片,再聽一遍廣播,這種音畫分離的觀感沒比 PPT 強多少。
后來在軟硬工程師的天才努力后,發(fā)明了一種神奇的東西--線程。說線程之前我們先說一下進程,這個東西可是我們能看到的東西,比如你啟動的瀏覽器,比如你正在使用的微信,這些軟件啟動后,在操作系統(tǒng)中都是一個進程。而線程呢?它可以簡單理解成是一個進程的子集,也就是說進程其實是一堆線程組成。而且操作系統(tǒng)通常會把所有硬件資源,包括內存之內的全分配給進程,進程就像一個包工頭一樣再分配給底下的線程。但是唯獨有一樣資源,操作系統(tǒng)是直接分配給線程的,那就是 CPU 資源。
這樣的設置其實是有深意的。可能有人覺得,分給進程也可以呀,但是進程要比線程重的多,切換的開銷過大,得不嘗試。就像是你想打開一個新的網(wǎng)頁,是打開一個新瀏覽器快呢?還是打開一個新的 Tab 頁快呢?總之有了線程之后,我們就有了一個很酷炫的操作--線程切換。他能帶來什么呢?接著說電影的事,我們其實還是先播視頻再放聲音。但是與上面不同的是,我們是先放一會視頻,再放一會聲音。只要單次播放的夠短,兩種操作之間的切換夠快,就會讓人感覺其實視頻與聲音是同時播的錯覺。而輕量的線程以及提供的切換能力給這種操作提供了可能。
至此,問題在無數(shù)硬件與軟件工程師的努力下,得到了比較完美的解決。
新的問題
事情到了這里,本該皆大歡喜、功德圓滿。結果英特爾又出來搞事,但其實他這次也是被逼無奈。
還記得我們上面說的以英特爾 CEO--高登·摩爾命名的摩爾定律嗎?這個定律其實并不是根據(jù)嚴謹?shù)目茖W研究得出來的,而是通過英特爾的過往表現(xiàn)推導出的這個結論。按理說這是極不符合科學規(guī)律的,就像我遇到的每個程序員都背個電腦包,但是我在大街上不能隨便看到一個背著電腦包的人就說他是程序員。但是英特爾就是這么 NB,他在的大街上全是程序員。英特爾就這樣維護著這個定律每 18 個月把 CPU 的性能翻一倍,持續(xù)了很多年。
直到第四任 CEO 的時候,摩爾定律突然不靈了,上圖就是時任英特爾 CEO--克瑞格·貝瑞特。在一次技術大會上,向與會者下跪。為一再延期直至最終失敗放棄的 ?4GHz 主頻奔 4 處理器致歉。
到此,摩爾定律終結,CPU 的發(fā)展進入了瓶頸。直到有一天一個腦門閃光的硬件工程師敲響了克瑞格·貝瑞特辦公室的大門。"老板你不用跪了,我有個辦法可以把 CPU 性能提高一倍"。
一句話讓克瑞格老淚縱橫,那一天,回想起了,受那些家伙支配的恐怖……被囚禁在鳥籠中的屈辱……
image.png克瑞格激動的問道:"什么方案?"
硬件工程師:"很簡單呀,我們只要把現(xiàn)在兩個的 CPU 裝到一個大號的 CPU 里面,那么他的性能就是兩個 CPU 的性能呀!我可真是一個小機靈鬼呢"
做了一輩子 CPU 的克瑞格,氣的差點進了 ICU。"我老克就算跪一輩子,也不會做這種傻事"。
image.png上圖為英特爾發(fā)布的 28 核 CPU。嗯?
當然上面其實有些戲謔的成分,但是 CPU 的發(fā)展結果也的確是往更多的核心數(shù)去發(fā)展。從單核到雙核再 6 核、8 核不停的增長核心數(shù),CPU 的性能也的確跟著增長。這其實跟我們軟件工程師常用的分布式架構一樣,當單機的性能達到了瓶頸,不可能再通過縱向的增加服務器的性能提高系統(tǒng)負載,只能通過把單機系統(tǒng),拆成多個分布式服務來進行橫向的擴展。
通過增加 CPU 的核心數(shù),硬件工程師看似圓滿的完成時代交給他的任務。結果一口大鍋甩在了咱們軟件工程師的頭上。
來,我們回顧一下,上面我們說 CPU、內存、IO 他們有一個核心矛盾,這個矛盾就是速度的差異。而且這個差異仍然沒有解決。但是我們變相的解決了。解決方案是什么?硬件工程師在 CPU 的核心里劃了一塊地方做為緩存,通過這個緩存均衡他們之間的差異。而軟件工程師呢,為了最大的提高 CPU 的利用率,搞了一個叫線程的東西,通過多線程之間的切換圓滿解決問題。
嗯,這個方案很完美,沒有問題。但是,前提是運行在單核的 CPU 下。
剛才我們說了 CPU 的核心,會有一塊地方緩存從內存里加載的數(shù)據(jù),這樣就不用每次從內存里加載了,提高了效率。但是呢,單核有一個緩存,多核就會出現(xiàn)多個緩存,再加上我們多線程的運行,會出現(xiàn)什么情況呢?下面我們以真實代碼為例子:
代碼很簡單,兩個線程都調用一個 add 方法,而這個 add 方法的操作是循環(huán) 10 w 次,每次都把這兩個線程共享的 count 變量加 1 。按照我們的直覺來說,count 開始是 0,每個線程加 10 w,總共兩個線程,所以 10 w * 2 = 20 w。
可是呢?結果并不是我們想的那樣,我運行的結果是:113595。而且每次運行的結果都不一樣,你可以試試。結果基本上都在 10w ~ 20w 之間,而且無限趨向于 10w。
這是什么鬼?還記得前面說的 CPU 緩存嗎?沒錯,他就是這只鬼。為了便于說明問題,我畫了幾張圖。
上圖是在單核的情況下,首先這個 count 會被加載到內存中。這時他是初始值 0。然后如圖所示,第 1 步他被加載到了 CPU 的緩存中,CPU 處理器把他從緩存中取出來,然后進行 add 操作,加完之后再放入緩存中,緩存再把 count 寫入內存中,最終我們就得到了結果。可見單核情況下,因為共享緩存與內存,沒有任何問題,我們接著看多核的情況下。
如上是多核場景下的運算過程,具體步驟如下:
首先 count 被加載到內存,緊接著線程 1 被 CPU 1 調用,把內存的 count = 0 加載到了緩存中
然后 CPU 1 把緩存中 count = 0 加載到處理器中,一個時間片處理后 ?13595
CPU 把 count =?13595 存入到緩存,準備下次接著算
緩存 把 count ?= 13595 刷新加內存,等下個時間片再加載
線程 2 得到了 CPU2 時間片,從內存中把剛剛線程 1 算了一半的 ?count? = 13595 加載到了緩存
CPU 2 把 count ?= 13595 加載到了處理器,開始運算。與此同時 CPU 1 把時間片又分配給了線程 1,線程接著剛才的 ?count = 13595 運算,很快算完得到 10 w ,并把結果最終刷進了內存,現(xiàn)在內存中的數(shù)據(jù)為 count = 10w。
線程 2 也很快運行完了 10w 次,現(xiàn)在他得到的結果 ?13595 + 10w = 113595。然后同樣把結果最終的刷新進了內存,現(xiàn)在內存中的數(shù)據(jù)為 count = 113595。
看到問題了嗎?可以理解緩存中的 count 是內存中的 count 的一份拷貝。在緩存中修改時并不會變更內存中的值,而是過一段時間后刷新回內存,而線程 1 把計算了一半的值,刷新進內存后,線程 2 把這個新值加載到了 CPU2 中,然后計算。與此同時 CPU 1 完成了計算,并把值刷新進了內存,CPU2 仍在計算,因為他不知道 CPU1 把值改變了,計算完了,把自己計算的值也刷新進了內存中,這樣就把剛剛 CPU1 忙乎半天的結果覆蓋了。
出現(xiàn)這個問題的根本原因就是,CPU 1 與 CPU 2 各自的操作對于雙方不可見。在這種情況下,運行期間其實總共有 3 個 count 變量,一個是內存中的 count,一個是 CPU1 中的 count 拷貝,最后一個是 CPU2 中的 count 拷貝。
結論
硬件工程師為均衡 CPU 與 緩存之間的速度差異,而特意加的 CPU 緩存,竟然在多核的場景下陰差陽錯的成為了并發(fā)問題中可見性的根源
最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,目前,已經(jīng)拿到了大廠offer,拿去不謝!
下載方式
1.?首先掃描下方二維碼
2.?后臺回復「Java面試」即可獲取
《新程序員》:云原生和全面數(shù)字化實踐50位技術專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的【并发那些事】可见性问题的万恶之源的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 推荐 8 个常用 Spring Boot
- 下一篇: 不好意思,我真的不知道MySQL的窗口函