提到隊列,我們會在很多地方聽到或者看到,
那我們來看一下這位不太說話的老朋友,
從棧很容易聯想到隊列的實現
- 棧是先進后出的數據結構,隊列而言它是先進先出。
- 對棧而言,在棧頂有一個指針即可。
- 隊列是需要兩個指針,一個在隊頭,一個在隊尾。對應着入隊操作和出隊操作。
- 基於數組實現的是順序隊列,基於鏈表實現的是鏈式隊列。
一個數組實現的順序隊列,在 入隊了 AA 、BB 、CC 后,
隊頭指針 head=0,隊尾指針 tail=3。如下圖:
緊接着,又有兩次出隊,同樣,對於出隊head指針往后移動兩個:
以上兩個圖對應的如隊出隊操作,也是很容易看出問題所在:
隨着入隊出隊一波操作,tail指針很容易移動到最后的位置,表面上不能再入隊了。
但是極有可能如圖二一樣,頭指針head前面有大片空地。
怎么辦?搬!我在出隊之后,后面的數據往前挪,我們可以稱之為移動補位。
但是每一次出隊操作都去搬數據,時間復雜度想想就會很高 O(n)
怎么優化?
tail指針抵達末尾,同時head指針不在隊頭。也就是tail到了最后,且head前面有空。
此時觸發數據搬移,過程如下:
人的思想不斷進步,並且思考如何做得更加輕巧靈活。
我們會思考,可不可以不用搬移數據呢?
可以,接下來輪到循環隊列登場了。。。。。。
循環隊列,顧名思義。首尾相連形成環。噥,就是這個樣子:
長得這么好看,一定要對得起我們對它的期望。
經過一番出隊入隊,頭部索引=2,尾部指針指向最后一個位置,即將接受FF入隊,
此時看上去又到了挪動數組的時候了?
環形的存在就是為了避免隊列的數據搬移,我想你已經想到了它的靈巧之處。
對,就是將數據FF填充到索引=5處,tail指針移動到下一個,也就是索引=0處,就成了這樣:
隊列在平時工作時用的機會場景比較少,但是在一些偏底層系統中確實應用比較廣泛。
比如:阻塞隊列、並發隊列
阻塞隊列,就是在隊空時,取數據會被直接拒絕。直到有數據才會允許被訪問。
這種模型類似於 生產-消費關系,對的,這也是很多的消息隊列的思想和應用。
這種阻塞隊列可以協調生產和消費的關系。當然,也可以生產的i消息被多個消費。
這又產生了一個線程並發問題,我們如何保證線程安全呢?這就需要並發隊列。
基於數組的循環隊列+CAS原子操作,可以很好的實現無鎖並發隊列。
基於以上,微軟給我們所提供的這些源碼:
- 隊列 Queue ;
- 泛型隊列 Queue<T>;
- 阻塞泛型集合 BlockingCollection<T>
- 以及微軟強大的並行庫中的並發泛型隊列 ConcurrentQueue<T>
我們着重看一下泛型隊列和並發泛型隊列
隊列 Queue 、泛型隊列 Queue<T>
我們直接看一下泛型版本的:
0、注釋說明:這是一個基於數組實現的環形隊列,也就是循環隊列
1、初始定義
2、重要的私有變量
3、入隊:分為兩塊主邏輯,一個是隊滿,一個是正常插入。
第0步已經注釋說明這是一個循環隊列,所以我們借此機會分析一下這個循環隊列。
- 隊滿
if (_size == _array.Length) 2倍擴容並且有最小裝載量判斷。
- 正常
_tail = (_tail + 1) % _array.Length; 下面我們來看看這句話怎么來的。
對於非循環隊列,頭尾指針和數組的關系好確認。
而循環隊列,因為是一個環,所以怎樣定位移動后的指針位置才是關鍵的。
數組長度=6
當我入隊FF,原來尾部指針=5,當前尾部指針=0;
接着入隊GG, 原來尾部指針=0,當前尾部指針=1;
當我入隊HH,原來尾部指針=1,當前尾部指針=2;
規律:當前指針 = (原來指針 +1) % 數組長度
4、出隊同3
ConcurrentQueue<T>
注釋說的很明白,這是一個無鎖並發隊列
我們在看源碼之前先來了解一些定義
對於現在的多CPU、以及超線程概念的操作系統來說,CPU和內存之前存在處理速度上的差距,所以中間加了寄存器和高速緩存來緩沖。
多線程並發情況下,多核計算機,一個CPU讀取的是在寄存器中的值,另一個CPU讀取的是內存中的值,這就造成了數據不同步。
對於產生的並發問題,我們來看看並發隊列對這些的處理。
我們先來理解接下代碼中涉及到的名詞:
1、易失結構 volatile : 告訴編譯器和CLR不需要優化代碼順序,使得代碼可控。不用將字段緩存到寄存器,緩存早內存中就行。
2、互鎖結構 Interlocked : CAS保證原子性讀取操作
3、自旋鎖 :原地打轉,直到達到條件才離開。對於線程來講,一直持有資源不撒手。
4、線程類提供了幾個方法:
- Thread.Sleep(0):掛起自身,讓出剩余的時間片,強迫系統調度其他同級或者更高級的線程。
- Thread.Sleep(1):強迫進行一次上下文切換
- Thread.Ylied():提前結束剩余的時間片,使得同級或者低級線程可能被調度。
- Thread.SpinWait():超線程CPU模式下,強迫自身暫停,允許CPU調度其他線程。
5、CAS理論:compare and swap 比較並交換。該操作通過將內存中的值與指定數據進行比較,當數值一樣時將內存中的數據替換為新的值。
天也不早了,人也不少了,讓我們干點正事。簡單看看入隊和出隊操作。
入隊:
需求是怎樣保證入隊的原子性?
通過 Interlocked 聲明同步塊,只允許一個線程搶占資源進行入隊,其他線程使用自旋鎖進行原地等待。
等當前線程釋放同步塊,其他線程再次搶占同步塊,然后入隊。直到隊滿跳出。
- 下面這是聲明了自旋鎖,線程進行入隊搶占。
- m_high =-1
- m_high 通過 Interlicked CAS原子操作,遞增。進行入隊或者隊滿判斷。
出隊:也是類似,通過自旋鎖,搶占同步塊進行原子性出隊操作。
最后我們再來悄悄看看 自旋鎖自旋邏輯:
自旋至少10次,然后進行相應的自旋等待,並且相應的讓出自己的時間片,讓其他低級別線程可以得到調度。
總體來說,並發隊列通過CAS進行原子性入隊和出隊,並結合自旋鎖進行搶占資源。
也就是很多的線程並發入隊或者出隊,同一時刻只有一個可以進行原子性入隊出隊。