圖解--隊列、併發隊列

提到隊列,咱們會在不少地方聽到或者看到,數組

那咱們來看一下這位不太說話的老朋友,緩存

從棧很容易聯想到隊列的實現安全

  • 棧是先進後出的數據結構,隊列而言它是先進先出。
  • 對棧而言,在棧頂有一個指針便可。
  • 隊列是須要兩個指針,一個在隊頭,一個在隊尾。對應着入隊操做和出隊操做。
  • 基於數組實現的是順序隊列,基於鏈表實現的是鏈式隊列。

 

一個數組實現的順序隊列,在 入隊了 AA 、BB 、CC 後,數據結構

隊頭指針 head=0,隊尾指針 tail=3。以下圖:多線程

 

緊接着,又有兩次出隊,一樣,對於出隊head指針日後移動兩個:併發

 

以上兩個圖對應的如隊出隊操做,也是很容易看出問題所在:優化

隨着入隊出隊一波操做,tail指針很容易移動到最後的位置,表面上不能再入隊了。spa

可是極有可能如圖二同樣,頭指針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、註釋說明:這是一個基於數組實現的環形隊列,也就是循環隊列

一、初始定義

 

二、重要的私有變量

 

三、入隊:分爲兩塊主邏輯,一個是隊滿,一個是正常插入。

 

 第0步已經註釋說明這是一個循環隊列,因此咱們藉此機會分析一下這個循環隊列。

  • 隊滿 
    if (_size == _array.Length)  2倍擴容而且有最小裝載量判斷。
  • 正常
   _tail = (_tail + 1) % _array.Length; 下面咱們來看看這句話怎麼來的。

 對於非循環隊列,頭尾指針和數組的關係好確認。

 而循環隊列,由於是一個環,因此怎樣定位移動後的指針位置纔是關鍵的。

 

數組長度=6

當我入隊FF,原來尾部指針=5,當前尾部指針=0;

接着入隊GG,  原來尾部指針=0,當前尾部指針=1;

當我入隊HH,原來尾部指針=1,當前尾部指針=2;

規律:當前指針 = (原來指針 +1) % 數組長度 

四、出隊同3

 

ConcurrentQueue<T>

註釋說的很明白,這是一個無鎖併發隊列

咱們在看源碼以前先來了解一些定義

對於如今的多CPU、以及超線程概念的操做系統來講,CPU和內存以前存在處理速度上的差距,因此中間加了寄存器和高速緩存來緩衝。

多線程併發狀況下,多核計算機,一個CPU讀取的是在寄存器中的值,另外一個CPU讀取的是內存中的值,這就形成了數據不一樣步。

對於產生的併發問題,咱們來看看併發隊列對這些的處理。

咱們先來理解接下代碼中涉及到的名詞:

一、易失結構 volatile : 告訴編譯器和CLR不須要優化代碼順序,使得代碼可控。不用將字段緩存到寄存器,緩存早內存中就行。

二、互鎖結構  Interlocked : CAS保證原子性讀取操做

三、自旋鎖 :原地打轉,直到達到條件才離開。對於線程來說,一直持有資源不撒手。

四、線程類提供了幾個方法:

  • Thread.Sleep(0):掛起自身,讓出剩餘的時間片,強迫系統調度其餘同級或者更高級的線程。
  • Thread.Sleep(1):強迫進行一次上下文切換
  • Thread.Ylied():提早結束剩餘的時間片,使得同級或者低級線程可能被調度。
  • Thread.SpinWait():超線程CPU模式下,強迫自身暫停,容許CPU調度其餘線程。

五、CAS理論:compare and swap 比較並交換。該操做經過將內存中的值與指定數據進行比較,當數值同樣時將內存中的數據替換爲新的值。

 

天也不早了,人也很多了,讓咱們乾點正事。簡單看看入隊和出隊操做。

入隊:

需求是怎樣保證入隊的原子性?

經過 Interlocked 聲明同步塊,只容許一個線程搶佔資源進行入隊,其餘線程使用自旋鎖進行原地等待。

等當前線程釋放同步塊,其餘線程再次搶佔同步塊,而後入隊。直到隊滿跳出。 

 

  • 下面這是聲明瞭自旋鎖,線程進行入隊搶佔。

 

  • m_high =-1 

 

  • m_high 經過 Interlicked CAS原子操做,遞增。進行入隊或者隊滿判斷。

 

出隊:也是相似,經過自旋鎖,搶佔同步塊進行原子性出隊操做。

 

最後咱們再來悄悄看看 自旋鎖自旋邏輯:

自旋至少10次,而後進行相應的自旋等待,而且相應的讓出本身的時間片,讓其餘低級別線程能夠獲得調度。

 

整體來講,併發隊列經過CAS進行原子性入隊和出隊,並結合自旋鎖進行搶佔資源。

也就是不少的線程併發入隊或者出隊,同一時刻只有一個能夠進行原子性入隊出隊。

相關文章
相關標籤/搜索