這篇文章的目的並不是想教你如何造火箭(面試造火箭,工作擰螺絲),而是想通過對原理和應用案例的有限度剖析來協助你構建起併發的思維,並將作業系統的理論知識與工程實踐結合起來,貫穿從學到會的全過程。當然,雖然我們是從實用角度出發,但具有實踐意義的深層次知識點永遠會是面試中的殺手鐗,這可比只能口頭造火箭的理論知識更吸引面試官。
本文適合誰:
- 希望能瞭解併發概念的初學者
- 需要理清併發概念與技術的工程師
- 對併發在工作中的應用與其底層實現原理感興趣的讀者
在這篇文章中,你將瞭解到併發與多執行緒相關的一系列概念,通過一些例子我們可以在不糾結於具體的技術細節的情況下形成對併發與多執行緒相關的各種概念的抽象理解。有了這些概念以後,我們再去學習具體的理論和技術細節就是手到擒來的事了。
什麼是併發?
最近幾年淘寶發展得如火如荼,湧現出了一大批白手起家的賣家。想象一下你是一個剛剛起步的小賣家,自己運營一個服裝網店,每天都要自己打包發貨。剛開始時生意一般,每天自己一個人一個小時就能幹完。
隨著生意的蓬勃發展,發貨時間慢慢地從一個小時漲到了兩個小時、四個小時,一次因為延遲發貨導致被投訴之後,你終於覺得該招更多的人了。很快,兩個小夥伴加入了你的事業,打包速度開始有了質的提高。這就是最基本的併發了,每個人都可以看成是一個執行緒,同樣的工作量,幹得人多了自然就快了。
所以併發
就是通過多個執行器同時執行一個大任務來縮短執行時間、提高執行效率的方法。
資料競爭
但是好景不長,週末一盤貨,你發現少了不少。這辦公室裡也沒遭賊,怎麼就會少貨呢?細細一查快遞單,你發現竟然有幾單發重了。之後的幾天你都細細留意了一下發貨的過程,最後發現是因為每個人都會拿著一張發貨清單去備貨,如果有一些訂單不小心列印重複了,就有可能會被不同的人重複發貨。雖然數量不多,但是也很心痛啊。這個問題產生的原因就是因為每個人在備貨之前拿到的訂單狀態(未發貨)在實際備貨時發生了變化(已由其他人發貨)。這種對一個共享資料(訂單的發貨狀態)本應獨佔的讀取、檢查、修改過程,如果發生了併發,這種情況就被稱為資料競爭
。而這個讀取、檢查、修改的過程就被稱為臨界區
,臨界區
指的就是一個存在資料競爭
的程式碼片段。
資料競爭出現的根本原因是一個資料本來應該只能由一個執行器完整地執行讀取、檢查、修改過程,但是如果出現了併發,那麼就沒辦法保證到了“修改”這一步時的資料還保持了“讀取”時的值了。
確定原因後,有人想到了一個好辦法,可以列印一張總的發貨清單,這樣所有人都必須以這個清單上的訂單是否發貨來確定是否要對訂單進行備貨併發貨了。因為清單隻有一份,所以每次只能由一個人來修改訂單的發貨狀態。這種只能由一個執行器進行資料修改操作來避免發生資料競爭
問題的做法就被稱為互斥
,也就是我們常說的鎖
了。
分散式併發概念
分散式
因為你管理得當,生意發展得很快,現在的辦公室裡已經堆不下所有衣服了。所以你又租了一個倉庫來同樣進行發貨。兩個地方都會進行發貨,那麼就可以把每一個倉庫理解為一臺獨立的計算機,這樣通過多臺計算機完成同一任務的方式就可以被稱為分散式
,這樣的一組計算機的集合就被稱為叢集
。
這時候之前用一張紙質的總髮貨清單的資料競爭
解決方式就行不通了,所以我們需要把這張總髮貨清單放到雲端,讓大家可以通過網路進行編輯,但是每次只能一個人編輯。在這種情況下,我們可以把兩個倉庫各自看成一臺計算機/程式,而每個倉庫裡的人就是這個程式中的執行緒。這樣的話這張總髮貨清單就成為了一個分散式鎖
,因為它每次只能有一個人編輯,所以是一個互斥鎖
,或者簡稱為鎖
;而因為它可以被兩個程式/計算機(倉庫)共同使用,所以被稱為是分散式鎖
。
什麼是程式/執行緒?
可以簡單地將程式理解為我們電腦/手機上的一個應用,同一臺手機上的每個App都是一個程式,同一個App在每個手機上也是一個程式。程式和程式之間可以理解為是兩個倉庫,互相之間物理隔離;而執行緒就是倉庫裡的每一個人,他們共享同一個辦公空間。這裡的辦公空間就可以理解為作業系統中的虛擬記憶體空間,但是本文主要討論併發相關的概念,就不繼續展開了。
分散式資料不一致
因為生意比較好,所以所有人都很忙。有時候就會因為有一些人雖然在雲端表格上已經勾上了一個訂單,但是一忙就給忙忘了。其他人怕重複發貨又不會再去處理已經勾上的訂單了,因為這樣導致的未發貨訂單讓店鋪被投訴了好多次,影響非常大。這種在併發過程中修改了資料狀態但是沒有完成後續執行的情況就會出現資料不一致
,即訂單已經被勾上,但實際並沒有發貨。
但是作為聰明的老闆,你又想到了解決的方法。每隔一小時兩個倉庫就會各派一個人檢查一下已經勾上的訂單是否已經都打包完貼上快遞單了。這種每隔一段時間就檢查並處理遺漏的資料不一致
訂單的任務就被稱為兜底任務
。而通過兜底任務
實現的在最後所有訂單都會達到資料一致狀態的情況就被稱為最終一致性
。
優化方式
大家可能早就覺得前面介紹的總髮貨清單的方法太傻了,只要每個訂單都只列印一張發貨清單,由單獨一個人去負責分發清單就可以了,其他人只要處理好自己被分配到的訂單就可以了。最後再加上一個兜底任務對訂單的發貨情況進行二次校驗基本上就不會發生漏發或者重發的情況了。這種由一個執行器進行任務拆分,再由一組執行器進行處理,最後再由一個或一組執行器進行結果彙總的處理方式就是現在非常流行的map-reduce
方法了。這種方法在大資料或者程式語言標準庫裡都有大量的應用,比如大資料領域赫赫有名的Hadoop和Java語言中的ForkJoinPool
都使用了這種思想。
回顧
在這篇文章中,我們涉及到了以下的技術名詞:
- 併發,通過多個執行器同時執行一個大任務來縮短執行時間、提高執行效率的方法。
- 資料競爭,對一個共享資料本應獨佔的讀取、檢查、修改過程發生了併發的情況。
- 臨界區,存在
資料競爭
的程式碼片段。 - 互斥鎖(也可以簡稱為“鎖”),同一時間只能由一個執行器獲取的實體,用於實現對臨界區的互斥(只有一個)訪問。
- 分散式,通過多臺計算機完成同一任務的方式。
- 叢集,一組完成同一任務的機器。
- 分散式鎖,在不同機器/程式上提供互斥能力的鎖。
- 資料不一致,一系列操作不具有原子性,一部分執行成功而另一部分沒有,導致不同資料之間存在矛盾,例如訂單已經是發貨狀態,但是實際沒有發貨。
- 兜底任務,處理
資料不一致
狀態的任務。 - 最終一致性,通過
兜底任務
或其他方式保證資料不一致
的情況最終會消失。 - map-reduce,一種任務拆分-執行-再合併的任務執行方式,可以有效地利用多臺機器、多核CPU的效能。
後記
因為併發的知識範圍很大,而且對於一些抽象概念的傳遞必然會需要花費一些篇幅,所以這個主題將會包含一系列文章,主要覆蓋以下主題:
- 什麼是併發?
- 拋開冗長繁雜的技術點,直接理解併發相關的各種概念。
- 什麼是多執行緒?
- 多執行緒是併發的一種重要形式。通過具體的多執行緒問題引出多執行緒程式設計中的關鍵點和對應的工具與知識點,輕鬆學會多執行緒程式設計。
- 常用工具中的併發實現
- 通過解析知名開源工具中的併發方案實現來深入理解併發程式設計實踐。
有興趣的讀者可以繼續關注後續的文章,在之後的文章中會有對併發程式設計、作業系統原語、硬體原語等等理論與實踐知識的詳細介紹與案例。
對資料庫索引感興趣的讀者可以瞭解一下我之前的文章:
- 資料庫索引是什麼?新華字典來幫你 —— 理解
- 資料庫索引融會貫通 —— 深入
- 20分鐘資料庫索引設計實戰 —— 實戰
- 資料庫索引為什麼用B+樹實現? —— 擴充套件