【趣話計算機底層技術】一個故事看懂各種鎖
我是一個線程,一個賣票程序的線程。
自從我們線程誕生以來,同一個進程地址空間里允許有多個執(zhí)行流一起執(zhí)行,效率提升的同時,也引來了很多麻煩。
我們賣票線程的工作很簡單,比如票的總數(shù)是100,每賣一張就減1,直到變成0售完為止。
以前單線程的時候沒啥問題,但多個線程一起執(zhí)行的時候就發(fā)現(xiàn),有些家伙讀取到票數(shù)是100,減1后變成99,還沒等他把票數(shù)寫回去,另外有別的線程去讀也是100,也做了同樣的事,結果賣了兩張票,票數(shù)才減了1張,一天下來,多賣了很多票,氣的人類差點想砸了我們。
原子操作
我們把這問題反饋給了操作系統(tǒng)大哥,他給我們的解決方案是:讀取票數(shù)->票數(shù)減1->寫回票數(shù)這三個步驟不能被拆分,中途不能被打斷,他說這個叫原子操作。

他給了我們一套原子操作的手冊,里面不止有減法,還有加法、位運算,只要調(diào)用手冊里的原子操作函數(shù),就能保證邏輯的正確性。
我很好奇操作系統(tǒng)大哥是如何實現(xiàn)這個過程的原子化的,他告訴我,如果CPU只有一個核很好辦,執(zhí)行原子操作的時候,他不切換線程就可以。而如果是有多個核,就需要CPU來幫忙了。
你還別說,我們用這個原子操作來賣票后,再也沒有發(fā)生超額賣票的問題。
自旋鎖
有一天,我們賣票程序進行了升級,不再是直接讀取票數(shù)->票數(shù)減1->寫回票數(shù)這么簡單,還需要安排座位,現(xiàn)在變成了:

我們一翻手冊,沒有哪個原子操作函數(shù)能滿足我們的功能,畢竟安排座位這個操作是咱們賣票程序自己的事兒,一點也不通用,操作系統(tǒng)大哥肯定不會專門為我們開發(fā)一個原子操作函數(shù)。
我們只好再一次求助操作系統(tǒng)大哥,他一看就說:“你們這個問題,用自旋鎖就可以解決”
鎖?我們還是第一次聽說這玩意,不知道是什么意思。
操作系統(tǒng)告訴我們,讓我們回去創(chuàng)建一個鎖,這鎖里面有一個狀態(tài)標記來表示當前有沒有被占用,所有線程在執(zhí)行賣票操作之前,都得先去獲取這個鎖,如果鎖被占用了,線程就會阻塞在獲取函數(shù)那里,獲取函數(shù)內(nèi)部會不斷循環(huán)去檢查,直到別的線程釋放后才返回。

因為獲取鎖的時候線程會一直循環(huán)檢查狀態(tài),所以這鎖也叫自旋鎖。
現(xiàn)在,我們的工作流程變成了:

我們又可以愉快地賣票了!
互斥鎖
我們的業(yè)務發(fā)展很快,后來,我們用上了數(shù)據(jù)庫,把票的數(shù)量寫到了數(shù)據(jù)庫里面,于是我們的工作流程變成了:

本以為只是把票數(shù)從本地內(nèi)存搬到了數(shù)據(jù)庫,應該沒什么不一樣,結果發(fā)現(xiàn)我們運行經(jīng)常出錯,還莫名其妙地被殺掉進程。
我們向操作系統(tǒng)大哥大倒苦水,沒想到他卻說:“你們還好意思訴苦,你們獲取自旋鎖后搞那么耗時的操作,讓別的線程一直自旋等待,把CPU都跑得飛起,風扇都轉(zhuǎn)個不停···”
我們都羞愧地低下了頭,原來,把票數(shù)從本地內(nèi)存搬到了數(shù)據(jù)庫,差別這么大。
操作系統(tǒng)又接著說道:“自旋鎖因為會使得線程一直阻塞自旋,沒有讓出CPU,所以只適合處理比較快速的場合,像讀取數(shù)據(jù)庫這種很耗時的操作,不能用它,會白白浪費CPU時間!”
我們又詢問:“有沒有別的不浪費CPU的辦法呢?”
操作系統(tǒng)大哥又給我們介紹了一個叫互斥鎖的東西,聽說獲取這個鎖的時候,線程不會去自旋檢查,而是把自己放到這把鎖的等待隊列中,然后就交出CPU執(zhí)行權限,進入睡眠,看起來就跟阻塞一樣,等到后面別的線程釋放鎖之后,再去喚醒它的等待隊列里的線程繼續(xù)運行。
回去以后我們就用上了互斥鎖,現(xiàn)在我們的流程變成了這樣:

我們又又能愉快地賣票了!
條件變量
有一天,我們的賣票程序又進行了升級,21-100號票價格比較便宜,交給其他線程來賣,1-20號票價格比較貴,交給我來賣。
現(xiàn)在,我們不同的線程賣的票不一樣了。
別的線程的流程是這樣:

而我的流程是這樣:

使用互斥鎖倒也沒什么問題,可就是我經(jīng)常拿到鎖以后發(fā)現(xiàn)票號還大于20,不該我處理,只好默默的釋放鎖,白白把我喚醒,卻什么也沒干!
空手而回的次數(shù)多了以后,我又去請教操作系統(tǒng)大哥,能不能讓我指定一個條件,等條件滿足了再喚醒我運行,別讓我白跑。
沒想到還真有辦法!操作系統(tǒng)告訴了我一個叫條件變量的東西,等待條件變量的線程平時阻塞著,別的線程發(fā)現(xiàn)條件滿足之后,就將條件變量激活,那個時候等待的線程才會被喚醒。
回去之后,我跟我的小伙伴兒們商量了一下,我們創(chuàng)建了一個條件變量,等到它們發(fā)現(xiàn)票號小于等于20的時候,就把條件變量激活,我就會被喚醒,再也不用白跑了!
信號量
互斥鎖和條件變量真是好東西,幫了我們大忙,不僅幫我們解決了賣票的問題,我們還在其他很多地方使用它,我們遇到的絕大多數(shù)同步和互斥問題都可以用它們來解決。
直到有一天,我們遇到了一個新的問題。
我們的票越賣越好,從100到1000,票的數(shù)量越來越多,來找我們買票的客戶也越來越多。
因為每次售票都要訪問數(shù)據(jù)庫,連接它的線程有些多,那家伙有些吃不消了。
希望我們控制一下訪問數(shù)據(jù)庫的線程數(shù)量。
我們很自然的想到了互斥鎖,只有拿到鎖的線程才能去訪問數(shù)據(jù)庫。
可這互斥鎖名叫互斥,只能允許一個線程拿到鎖,總不能只允許一個線程訪問數(shù)據(jù)庫吧,那可不行。所以我們希望這個名額能放寬,允許多個線程同時獲得鎖。
我們再一次找到了操作系統(tǒng)大哥,大哥拿出了他的絕招——信號量。
他告訴我們,這信號量就像一個升級版的互斥鎖,它里面有一個計數(shù)器,可以用來指定最多允許多少個線程同時獲得它。
這正是我們想要的鎖!
很快我們用上了信號量,我們又又又能愉快地賣票了!
Tips:在信號量一節(jié)中,實際上數(shù)據(jù)庫能承受的并發(fā)量遠不止這點,這里為了故事情節(jié)需要,弱化了數(shù)據(jù)庫的并發(fā)承受能力。
好了,這一期的故事就講到這里了,如果你想查看更多未發(fā)布過的新鮮又有趣的技術小故事,可以掃描下方二維碼購買我最新出版的《趣話計算機底層技術》圖書。書中用一個個的小故事系統(tǒng)性的講解了計算機底層技術的基本原理,以及如何運用他們解決日常工作中的各種實際問題。

往期推薦

浙公網(wǎng)安備 33010602011771號