Michael Scott 非阻塞佇列演算法中的插入

2021-08-02 01:36:36 字數 2355 閱讀 6469

cas的基本使用模式:在更新某個值時存在不確定性,以及在更新失敗時重新嘗試。構建非阻塞演算法的技巧在於:將執行原子修改的範圍縮小到單個變數上。

鏈結佇列比棧更為複雜,因為它必須支援對頭節點和尾節點的快速訪問。因此,它需要單獨維護的頭指標和尾指標。有兩個指標指向尾部的節點:當前最後乙個元素的next指標,以及尾節點。當成功地插入乙個新元素時,這兩個指標都需要採用原子操作來更新。

這裡需要一些技巧來完成,第乙個技巧是,即使在乙個包含多個步驟的更新操作中,也要確保資料結構總是處於一致的狀態。這樣,當執行緒b到達時,如果發現執行緒a正在執行更新,那麼執行緒b就可以知道有乙個操作已部分完成,並且不能立即開始執行自己的更新操作。然後,b可以等待(通過反覆檢查佇列的狀態)並直到a完成更新,從而使兩個執行緒不會相互干擾。

雖然這種方法能夠使不同的執行緒「輪流」訪問呢資料結構,並且不會造成破壞,但如果乙個執行緒在更新操作中失敗了,那麼其他的執行緒都無法在訪問佇列。要使得該演算法成為乙個非阻塞演算法,必須確保當乙個執行緒失敗時不會妨礙其他執行緒繼續執行下去。因此,第二個技巧是,如果當b到達時發現a正在修改資料結構,那麼在資料結構中應該有足夠多的資訊,使得b能完成a的更新操作。如果b「幫助」a完成了更新操作,那麼b可以執行自己的操作,而不用等到a的操作完成。當a恢復後再試圖完成其操作時,會發現b已經替它完成了。

在下面的程式中,給出了 michael-scott 提出的非阻塞連界佇列演算法中的插入部分,它是由concurrentlinkedqueue實現的。在許多佇列演算法中,空佇列通常都包含乙個「哨兵節點」或者「啞(dummy)節點」,並且頭節點和尾節點在初始化時都指向該哨兵節點。尾節點通常要麼指向哨兵節點(如果隊列為空),即佇列的最後乙個元素,要麼(當有操作正在執行更新時)指向倒數第二個元素。下圖1給出了乙個處於正常狀態(或者說穩定狀態)的包含兩個元素的佇列。

michael-scott 非阻塞佇列演算法中的插入:

1

@threadsafe

2public

class linkedqueue11}

1213

private

final linkedqueue.nodedummy = new linkedqueue.node(null, null

);14

private

final atomicreference>head

15 = new atomicreference>(dummy);

16private

final atomicreference>tail

17 = new atomicreference>(dummy);

1819

public

boolean

put(e item) else35}

36}37}

38}39 }

圖1 處於穩定狀態幷包含兩個元素的對立

當插入乙個新的元素時,需要更新兩個指標。首先更新當前最後乙個元素的next 指標,將新節點鏈結到佇列隊尾,然後更新尾節點,將其指向這個新元素。在兩個操作之間,佇列處於一種中間狀態,如圖2。在等二次更新完成後,佇列將再次處於穩定狀態,如圖3所示。

實現這兩個技巧的關鍵在於:當佇列處於穩定狀態時,尾節點的next域將為空,如果佇列處於中間狀態,那麼tail.next 將為非空。因此,任何執行緒都能夠通過檢查tail.next 來獲取佇列當前的狀態。而且,當佇列處於中間狀態時,可以通過將尾節點移動乙個節點,從而結束其他執行緒正在執行的插入元素操作,並使得佇列恢復為穩定狀態。

圖2  在插入過程中處於中間狀態的對立

圖3 在插入操作完成後,佇列再次處於穩定狀態

linkedqueue.put 方法在插入新元素之前,將首先檢查佇列是否處於中間狀態(步驟a)。如果是,那麼有另乙個執行緒正在插入元素(在步驟c和d之間)。此時當前執行緒不會等待其他執行緒執行完成,而是幫助它完成操作,並將尾節點向前推進乙個節點(步驟b)。然後,它將重複執行這種檢查,以免另乙個執行緒已經開始插入新元素,並繼續推進尾節點,直到它發現佇列處於穩定狀態之後,才會開始執行自己的插入操作。

由於步驟c中的cas將把新節點鏈結到佇列尾部,因此如果兩個執行緒同時插入元素,那麼這個cas將失敗。在這樣的情況下,並不會造成破壞:不會發生任何變化,並且當前的執行緒只需要重新讀取尾節點並再次重試。如果步驟c成功了,那麼插入操作將生效,第二個cas(步驟d)被認為是乙個「清理操作」,因為它既可以由執行插入操作的執行緒來執行,也可以由其他任何執行緒來執行。如果步驟d失敗,那麼執行插入操作的執行緒將返回,而不是重新執行cas,因為不再需要重試——另乙個執行緒已經在步驟b中完成了這個工作。

這種方式能夠工作,因為在任何執行緒嘗試將乙個新節點插入到佇列之前,都會首先通過檢查tail.next是否非空來判斷是否需要清理佇列。如果是,它首先會推薦尾節點(可能需要執行多次),直到佇列處於穩定狀態。

阻塞 非阻塞

阻塞和非阻塞指 的是在接收和傳送時是否等待動作完成才返回 舉例 阻塞 block 是指,你撥通某人 的 但是此人不在,於是你拿著 等他回來,其間不能再用 非阻塞 nonblock 是指,你撥通某人 的 但是此人不在,於是你結束通話 待會兒再打。至於到時候他回來沒有,只有打了 才知道。即所謂的 輪詢 ...

阻塞非阻塞

阻塞和非阻塞 阻塞 可用在assign語句和always語句中,表示只要源訊號發生變化,目標訊號就立刻完成賦值操作,在always塊中,結果與語句順序有關,在always塊中是順序關係 非阻塞 只能用在always語句中,表示該語句結束時完成賦值操作,結果與語句順序無關,並行關係 可以這樣理解 阻塞...

阻塞 非阻塞 select epoll

著作權歸作者所有。首先我們來定義流的概念,乙個流可以是檔案,socket,pipe等等可以進行i o操作的核心物件。不管是檔案,還是套接字,還是管道,我們都可以把他們看作流。之後我們來討論i o的操作,通過read,我們可以從流中讀入資料 通過write,我們可以往流寫入資料。現在假定乙個情形,我們...