天天看點

ConcurrentDictionary線程不安全麼

本節的内容算是非常老的一個知識點,在.NET4.0中就已經出現,并且在園中已有園友作出了一定分析,為何我又拿出來講呢?理由如下:

(1)沒用到過,算是自己的一次切身學習。

(2)對比一下園友所述,我想我是否能講的更加詳盡呢?挑戰一下。

(3)是否能夠讓讀者了解的更加透徹呢?打不打臉不要緊,重要的是學習的過程和心得。

在.NET1.0中出現了HashTable這個類,此類不是線程安全的,後來為了線程安全又有了Hashtable.Synchronized,之前看到同僚用Hashtable.Synchronized來進行實體類與資料庫中的表進行映射,緊接着又看到别的項目中有同僚用ConcurrentDictionary類來進行映射,一查資料又發現Hashtable.Synchronized并不是真正的線程安全,至此才引起我的疑惑,于是決定一探究竟, 園中已有大篇文章說ConcurrentDictionary類不是線程安全的。為什麼說是線程不安全的呢?至少我們首先得知道什麼是線程安全,看看其定義是怎樣的。定義如下:

線程安全:如果你的代碼所在的程序中有多個線程在同時運作,而這些線程可能會同時運作這段代碼。如果每次運作結果和單線程運作的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

一搜尋線程安全比較統一的定義就是上述所給出的,園中大部分對于此類中的GetOrAdd或者AddOrUpdate參數含有委托的方法覺得是線程不安全的,我們上述也給出線程安全的定義,現在我們來看看其中之一。

對于GetOrAdd方法它是怎樣知道資料應該是添加還是擷取呢?該方法描述如下:

當給出指定鍵時,會去進行周遊若存在直接傳回其值,若不存在此時會調用第二個參數也就是委托将運作,并将其添加到字典中,最終傳回給調用者此鍵對應的值。

此時運作上述程式我們會得到如下二者之一的結果:

ConcurrentDictionary線程不安全麼
ConcurrentDictionary線程不安全麼

我們開啟兩個線程,上述運作結果不都是一樣的麼, 按照上述定義應該是線程安全才對啊,好了到了這裡關于線程安全的定義我們應該消除以下兩點才算是真正的線程安全。

(1)競争條件

(2)死鎖

就像女朋友說的哪有這麼多為什麼,我說的都是對的,不要問為什麼,但對于這麼嚴謹的事情,我們得實事求是,是不。競争條件是軟體或者系統中的一種行為,它的輸出不會受到其他事件的影響而影響,若因事件受到影響,如果事件未發生則後果很嚴重,繼而産生bug諾。 最常見的場景發生在當有兩個線程同時共享一個變量時,一個線程在讀這個變量,而另外一個變量同時在寫這個變量。比如定義一個變量初始化為0,現在有兩個線程共享此變量,此時有一個線程操作将其增加1,同時另外一個線程操作也将其增加1此時此時得到的結果将是1,而實際上我們期待的結果應該是2,是以為了解決競争我們通過用鎖機制來實作在多線程環境下的線程安全。

至于死鎖則不用多講,死鎖發生在多線程或者并發環境下,為了等待其他操作完成,但是其他操作一直遲遲未完成進而造成死鎖情況。滿足什麼條件才會引起死鎖呢?如下:

(1)互斥:隻有程序在給定的時間内使用資源。

(2)占用并等待。

(3)不可搶先。

(4)循環等待。

到了這裡我們通過對線程安全的了解明白一般為了線程安全都會加鎖來進行處理,而在ConcurrentDictionary中參數含有委托的方法并未加鎖,但是結果依然是一樣的,至于未加鎖說是為了出現其他不可預料的情況,依據我個人了解并非完全線程不安全,隻是對于多線程環境下有可能出現資料不一緻的情況,為什麼說資料不一緻呢?我們繼續向下探讨。我們将上述方法進行修改如下:

主程式輸出運作次數:

ConcurrentDictionary線程不安全麼

此時我們看到确确實實獲得了相同的值,但是卻運作了兩次,為什麼會運作兩次,此時第二個線程在運作調用之前,而第一個線程的值還未進行儲存而導緻。整個情況大緻可以進行如下描述:

(1)線程1調用GetOrAdd方法時,此鍵不存在,此時會調用valueFactory這個委托。

(2)線程2也調用GetOrAdd方法,此時線程1還未完成,此時也會調用valueFactory這個委托。

(3)線程1完成調用,并傳回JeffckyWang值到字典中,此時檢查鍵還并未有值,然後将其添加到新的KeyValuePair中,并将JeffckyWang傳回給調用者。

(4)線程2完成調用,并傳回cnblogs值到字典中,此時檢查此鍵的值已經被儲存線上程1中,于是中斷添加其值用線程1中的值進行代替,最終傳回給調用者。

(5)線程3調用GetOrAdd方法找到鍵key其值已經存在,并傳回其值給調用者,不再調用valueFactory這個委托。

從這裡我們知道了結果是一緻的,但是運作了兩次,其上是三個線程,若是更多線程,則會重複運作多次,如此或造成資料不一緻,是以我的了解是并非完全線程不安全。難道此類中的兩個方法是線程不安全,.NET團隊沒意識到麼,其實早就意識到了,上述也說明了如果為了防止出現意想不到的情況才這樣設計,說到這裡就需要多說兩句,開源最大的好處就是能集思廣益,目前已開源的 Microsoft.AspNetCore.Mvc.Core ,我們可以檢視中間件管道源代碼如下:

通過ConcurrentDictionary類調用上述方法無法保證委托調用的次數,在對于mvc中間管道隻能初始化一次是以ASP.NET Core團隊使用Lazy<>來初始化,此時我們将上述也進行上述對應的修改,如下:

此時将得到如下:

ConcurrentDictionary線程不安全麼

我們将第二個參數修改為Lazy<string>,最終調用valueFound.value将調用次數輸出到控制台上。此時我們再來解釋上述整個過程發生了什麼。

(3)線程1完成調用,傳回一個未初始化的Lazy<string>對象,此時在Lazy<string>對象上的委托還未進行調用,此時檢查未存在鍵key的值,于是将Lazy<striing>插入到字典中,并傳回給調用者。

(4)線程2也完成調用,此時傳回一個未初始化的Lazy<string>對象,在此之前檢查到已存在鍵key的值通過線程1被儲存到了字典中,是以會中斷建立,于是其值會被線程1中的值所代替并傳回給調用者。

(5)線程1調用Lazy<string>.Value,委托的調用以線程安全的方式運作,是以如果被兩個線程同時調用則隻運作一次。

(6)線程2調用Lazy<string>.Value,此時相同的Lazy<string>剛被線程1初始化過,此時則不會再進行第二次委托調用,如果線程1的委托初始化還未完成,此時線程2将被阻塞,直到完成為止,線程2才進行調用。

(7)線程3調用GetOrAdd方法,此時已存在鍵key則不再調用委托,直接傳回鍵key儲存的結果給調用者。

上述使用Lazy來強迫我們運作委托隻運作一次,如果調用委托比較耗時此時不利用Lazy來實作那麼将調用多次,結果可想而知,現在我們隻需要運作一次,雖然二者結果是一樣的。我們通過調用Lazy<string>.Value來促使委托以線程安全的方式運作,進而保證在某一個時刻隻有一個線程在運作,其他調用Lazy<string>.Value将會被阻塞直到第一個調用執行完,其餘的線程将使用相同的結果。

我們接下來看看Lazy對象。友善示範我們定義一個部落格類

接下來在控制台進行調用:

列印如下:

ConcurrentDictionary線程不安全麼

通過上述列印我們知道當調用blog.Value時,此時部落格對象才被建立并傳回對象中的屬性字段的值,上述布爾屬性即IsValueCreated顯示表明Lazy對象是否已經被初始化,上述初始化對象過程可以簡述如下:

列印結果和上述一緻。上述運作都是在非線程安全的模式下進行,要是在多線程環境下對象隻被建立一次我們需要用到如下構造函數:

通過指定LazyThreadSafetyMode的枚舉值來進行。

(1)None = 0【線程不安全】

(2)PublicationOnly = 1【針對于多線程,有多個線程運作初始化方法時,當第一個線程完成時其值則會設定到其他線程】

(3)ExecutionAndPublication = 2【針對單線程,加鎖機制,每個初始化方法執行完畢,其值則相應的輸出】

我們示範下情況:

結果列印如下:

ConcurrentDictionary線程不安全麼

奇怪的是當改變線程安全模式為 LazyThreadSafetyMode.ExecutionAndPublication 時結果應該為101和102才是,居然傳回的都是102,但是将上述blog.BogId++和暫停時間順序颠倒時如下:

此時兩個模式傳回的都是101和102,不知是何緣故!上述在ConcurrentDictionary類中為了兩個方法能保證線程安全我們利用Lazy來實作,預設的模式為 LazyThreadSafetyMode.ExecutionAndPublication 保證委托隻執行一次。為了不破壞原生調用ConcurrentDictionary的GetOrAdd方法,但是又為了保證線程安全,我們封裝一個方法來友善進行調用。

原封不動的進行方法調用:

最終正确列印隻運作一次的結果,如下:

ConcurrentDictionary線程不安全麼

本文轉自xsster51CTO部落格,原文連結:http://blog.51cto.com/12945177/1932191 ,如需轉載請自行聯系原作者

繼續閱讀