3

我一直在尋找的.NET TPL的「數據流」庫的某些部分出於好奇的執行情況和我遇到下面的代碼片段來了:線程安全

private void GetHeadTailPositions(out Segment head, out Segment tail, 
     out int headLow, out int tailHigh) 
    { 
     head = _head; 
     tail = _tail; 
     headLow = head.Low; 
     tailHigh = tail.High; 
     SpinWait spin = new SpinWait(); 

     //we loop until the observed values are stable and sensible. 
     //This ensures that any update order by other methods can be tolerated. 
     while (
      //if head and tail changed, retry 
      head != _head || tail != _tail 
      //if low and high pointers, retry 
      || headLow != head.Low || tailHigh != tail.High 
      //if head jumps ahead of tail because of concurrent grow and dequeue, retry 
      || head._index > tail._index) 
     { 
      spin.SpinOnce(); 
      head = _head; 
      tail = _tail; 
      headLow = head.Low; 
      tailHigh = tail.High; 
     } 
    } 

(這裏可查看:https://github.com/dotnet/corefx/blob/master/src/System.Threading.Tasks.Dataflow/src/Internal/ConcurrentQueue.cs#L345

從我對線程安全的理解中,這個操作很容易發生數據競爭。我將解釋我的理解,然後我認爲是'錯誤'。當然,我認爲這在我的心理模型中比在圖書館中更可能是一個錯誤,我希望這裏有人能指出我出錯的地方。

...

所有給定的字段(headtailhead.Lowtail.High)是揮發性。在我的理解這給出了兩個保證:

  • 每次所有四個字段被讀取,就必須讀取順序
  • 編譯器可能沒有的Elid任何的讀取和CLR/JIT必須採取措施爲了防止這些值

的「高速緩存」從我讀給定方​​法中,發生以下情況:

  1. ConcurrentQueue的內部狀態的初始讀取被執行(THA t是head,tail,head.Lowtail.High)。
  2. 執行單個忙等待旋
  3. 然後,該方法再次,並檢查讀出的內部狀態的任何變化
  4. 如果狀態已改變,則轉到步驟2,重複
  5. 返回讀取狀態一旦它被認爲是'穩定的'

現在假設全部正確,我的「問題」是這樣的:上述狀態的讀取不是原子的。我沒有看到阻止半寫入狀態的讀取(例如,寫入器線程已更新head但尚未tail)。

現在我有點意識到,像這樣的緩衝區中的半寫狀態不是世界的盡頭 - 在所有的headtail指針完全可以獨立更新/讀取之後,通常在CAS /自旋循環。

但是,我真的不知道什麼時候旋轉一下然後再讀一遍。你真的要在一次旋轉的時間裏「捕捉」正在進行的改變嗎?它試圖「防範」什麼?換句話說:如果整個狀態讀取的目的是爲原子的,我不認爲該方法做了什麼幫助,如果沒有,那麼究竟是什麼該方法在做什麼?

回答

2

你是對的,但請注意GetHeadTailPositions的輸出值在ToList,CountGetEnumerator之後被用作快照。

更令人擔憂的是併發隊列might hold on to values indefinitely。當專用字段ConcurrentQueue<T>._numSnapshotTakers不爲零時,它可防止將條目歸零或將其設置爲值類型的默認值。

斯蒂芬Toub在ConcurrentQueue<T> holding on to a few dequeued elements博客中提到這一點:

是好還是壞,這種行爲在.NET 4中實際上是這樣做的原因有枚舉語義做「設計」。 ConcurrentQueue < T>爲枚舉提供了「快照語義」,這意味着您開始枚舉的即時,ConcurrentQueue < T>捕獲隊列中當前內容的當前頭部和尾部,即使這些元素在捕獲後出隊或新元素在捕獲後被排隊,枚舉仍然會返回枚舉開始時的所有內容,並且只返回隊列中的內容。如果這些細分市場中的元素在出場時被淘汰,這將影響這些枚舉的準確性。

對於.NET 4.5,我們改變了設計以打擊我們認爲的良好平衡。出列的元素現在被取消,因爲它們已經被取出,除非發生併發枚舉,在這種情況下,元素不會被清空,並且會顯示與.NET 4中相同的行爲。所以,如果你永遠不會枚舉你的ConcurrentQueue < T>,那麼出隊將導致隊列立即刪除它對引出元素的引用。只有當發出出列隊列時,有人正在枚舉隊列(即在隊列上調用了GetEnumerator,而沒有遍歷枚舉器或處理它),那麼空值不會發生;與.NET 4一樣,在那一點上,引用將一直保留,直到包含的段被移除。

正如你可以從源代碼看,獲得一個枚舉(通過通用GetEnumerator<T>或非通用GetEnumerator),調用ToList(或ToArray其使用ToList)或TryPeek可能引起的引用被保持均勻去除物品後。不可否認,TryDequeue(其中稱爲ConcurrentQueue<T>.Segment.TryRemove)和TryPeek之間的競爭條件可能很難挑起,但它在那裏。

+0

那麼最終呢,這個方法的「雙重檢查」基本上沒有意義麼?或者我誤解了? – Xenoprimate

+2

可能需要的唯一檢查就是'_index',它可以避免在'Segment._next'鏈中返回不指向'_tail'的'_head'。我說*可能*,因爲在'_head'的易失性讀取之後,不應該可以觀察到具有'_tail' **的易失性讀取,因爲在不改變Segment._next '鏈,並在最後加上嚴格增加的指數。其他檢查有一定程度的穩定性(請參閱while語句前的註釋)。 – acelent