天天看點

多映射通用集合類(C#實作)--支援一鍵多值存儲

.net的通用Dictionary集合類有一個“鍵”唯一限制。考慮這樣一種情況:你想在Dictionary中存Author

Name以及Articles。首先,你想加入Bob->Article_Good_One,而當你想加入Bob->Article_Good_Second,你将得到一個異常。這是因為Dictionary的唯一鍵限制。Dictionary拒絕接受相同的key,因為它要求鍵唯一。

Dictionary類被設計成對搜尋具有很高的性能。而多映射類在你想讓搜尋具有很高的性能以及讓它可以為一個相同的鍵增加多個值的時候可以使用。

背景

Dictionary通用集合是一個很好的資料結構。很多的一種情況,在程式開發或者開發服務的時候,我們需要為一個通常的Key存儲多個value。一個簡單的例子就是一個人有多個電話号碼,而通路級别需要基于人。

使用Dictionary時,代碼大緻如下:

多映射通用集合類(C#實作)--支援一鍵多值存儲

現在讓我們來看一下,使用多映射類集合類,怎樣來實作這個簡單的例子:

多映射通用集合類(C#實作)--支援一鍵多值存儲

第一行建立了一個MultiMapBK對象。接下來的四行加入了四個人以及他們的電話号碼。然後,我們需要為這四個人設定其他的值。接下來的一些行用來檢索“Mai”以及列印所有的值。

多映射集合類的說明

下面的圖檔解釋了MultiMapBK的内部資料結構。

多映射通用集合類(C#實作)--支援一鍵多值存儲

就像上面的圖檔顯示的,每一個值都被存儲在一個List對象中。而List能夠為一個key存儲超過一個value。

而這個多映射集合類也是類型安全和線程安全的。它也支援當一個線程在修改集合時,另一個線程可以枚舉它。

什麼是一個多映射集合

它最基本的功能是為一個鍵存儲多個值,然而同時,它也是一個“并發”集合。下面的圖檔給出了一個多映射能夠實作的功能。

多映射通用集合類(C#實作)--支援一鍵多值存儲

使用代碼

使用這個集合類需要下面的幾個步驟:

1) 在VS2005或者2008的解決方案中,右擊“引用”

2) 點選“添加引用”同時加入下載下傳下來的MultiMap.dll【附于文章結束的連結中,自行下載下傳】

現在,讓我們來看一下,如何使用這段代碼

1、 怎樣定義以及為該集合加入元素:

多映射通用集合類(C#實作)--支援一鍵多值存儲

上面的第一行建立了一個新的集合對象。2,3,4行加入了幾個元素。注意,鍵“Alice”在所有的三個記錄中是相同的。

2、 怎樣從集合中讀取元素

多映射通用集合類(C#實作)--支援一鍵多值存儲

1,2,3,4行建立以及執行個體化了一個集合,第五行獲得了一個枚舉器來枚舉集合中的每一個元素。第七行,使用了MoveNext()來讀取一個特殊鍵(Alice)的每一個值。

3、 怎樣來快速定位一個特别的項并列印出其所有的值

多映射通用集合類(C#實作)--支援一鍵多值存儲

多映射集合的設計

内部使用的資料結構

基本的内部結構如下圖:

多映射通用集合類(C#實作)--支援一鍵多值存儲

如上面所說的,Dictionary<Key, List<Value>>是用來為一個相同的鍵存儲多個值得資料結構。

線程通路場景一

和通常的設計一樣,為了線程安全,集合類在增加,修改,以及删除的時候是被鎖住的。它産生了一個很小的(通常是毫秒級别的)開銷。但是為這個類的使用者提供了巨大的靈活性。這樣這個集合類的使用者就不需要再擔心線程安全,同步等問題。

考慮這樣一種情況:線程1正在讀取集合中第五鍵的值。現在,線程2将該值删除。那麼線程一的下一次讀取應該傳回null。鎖住内部的資料結構可以完成這個功能。

線程通路場景二

下一個場景是線程感覺。考慮這樣一種情況,兩個不同的線程都正在枚舉同一個鍵,該鍵有100個值,正如下面的圖檔顯示的:

多映射通用集合類(C#實作)--支援一鍵多值存儲

現在,當線程2被執行個體化來讀取Key_5的值的時候,線程1已經讀取了該鍵的5個值。而現在,下一次對MultiMap's

GetCurrentItem方法的調用,需要知道傳回哪一個值。例如,是對線程1來講的第六個值還是對線程2來講的第一個值。為了應對這樣的情況,MultiMap存儲了線程的明細在一個獨立的集合中。它檢索一個值來檢視哪個值是被哪個線程最後一次讀取的。使用線程的明細,MultiMap将能夠為正确的線程存儲正确的值。而用多線程執行下面的代碼,也仍然是正确的:

多映射通用集合類(C#實作)--支援一鍵多值存儲

線程通路場景三

下一步是完成在枚舉時的線程安全。

考慮如下的場景,線程一使用MoveNext()來擷取Current屬性。與此同時,線程2删除了被線程1“命中”的目前項。如果這個場景不被處理,我們将會線上程1上看到一個有趣的“NullReferenceException”異常。一個基本的想法就是:當線程1調用MoveNext()方法的時候,讓它擁有該Current屬性。然而,當線程2企圖删除線程1的Current時,有兩個選擇提供給線程2。選擇1:線程2可以阻塞“删除”,直到線程一調用MoveNext方法或者移動到下一項時,删除調用才傳回。選擇2:線程2可以采用不阻塞地調用删除方法,并且調用可以立即傳回。但該項将被辨別為“删除項”,但此時仍然存在。當線程1從該被辨別的項上移走時,該項将會被安全地從集合中删除。

為了簡便起見,讓我們離開這些并發場景。然而,主要的并發問題被列舉在下一節中。

解決并發問題

讨論所有的并發問題的處理,将會使這篇文章變得非常長。然而,列舉出那些需要被處理的并發問題,卻是很有用的:

1) 多線程更新(或者插入),在同一個MultiMap集合的執行個體上。項以它們接受到的順序添加。

2)

在不同的線程之間枚舉。例如,一個線程建立了一個枚舉器,但是被傳遞給另一個線程使用?允許,甚至允許在更多的線程之間共享同一個枚舉器的執行個體,每一個線程将獲得它自己正确的“next_item”(在方法MoveNext上)和Current。

3)

一個線程從集合中删除一項,而此時另一個線程正在枚舉或者讀取相同的項?允許。一旦線程通過讀取元素的MoveNext方法,或thread_abort或thread_close離開該元素,那麼删除将生效。然而,有趣的是,當删除這個項的線程再次枚舉該删除的項(通過Reset方法),那将不能看到被删除的項。這也将應用在任何{除了正在讀取被删除元素的線程之外的}所有線程。

4) 多線程同時删除同一個項?如果沒有其他的線程正在通路該元素,它将被安全地删除。

5) 一個線程正在删除一個元素,而另一個線程正在增加一個同名的元素?這依賴于哪一個線程首先獲得“鎖”。

6) 一個線程通路一項,然後終止了。之後的某個時間,另一個線程企圖删除這項?該項将會被删除。

7) 一個線程删除了一項,而另一個線程此時正在讀該項。同時第三個線程,企圖枚舉該項?該項對第三個線程将是不可見的。

8) 為什麼在API中沒有暴露Count屬性(元素的個數)?是為了避免糟糕的代碼。

為什麼線程安全的枚舉是重要的

我們有一個UI來列印從Dictionary集合中的值。為了讓該集合會有一些比較靈活的(或者比較新的)值,我們用一個線程來更新它。現在,使用者按下重新整理鍵。跟随下面的圖檔中基于紅色序列号的事件。結果是有一個InvalidOperationException異常将被Dictionary集合的枚舉器抛出。

讓我們通過鎖住“枚舉”以及“更新”操作來解決這個異常。但是考慮這個解決方案,當使用者按下重新整理按鈕時,背景的更新仍然在繼續。而UI将被挂起。這個行為是不可取的。

現在,考慮相同的場景下使用MultiMap集合。UI線程重新整理以及更新線程可以被同步執行。下面的圖檔解釋了,這個場景:

多映射通用集合類(C#實作)--支援一鍵多值存儲

下面的代碼是一個用Dictionary類做的一個簡單的例子:

多映射通用集合類(C#實作)--支援一鍵多值存儲
多映射通用集合類(C#實作)--支援一鍵多值存儲

MultiMap通過提供一種内建的線程安全機制,下面的更簡短的代碼以及解決了這個問題:

多映射通用集合類(C#實作)--支援一鍵多值存儲

輸出:

多映射通用集合類(C#實作)--支援一鍵多值存儲

有什麼不同

使用一個鎖住的Dictionary集合與使用MultiMap集合有什麼差別?

1) 多線程枚舉

這意味着什麼?通常,枚舉器不能夠在一個線程上使用,當另一個線程正在更新相同的集合時。MultiMap集合則支援這樣的操作。不同的線程可以更新、修改以及枚舉MultiMap集合。

2) 線程感覺枚舉器

這又意味着什麼?考慮到Dictionary集合在一個WCF服務中使用。通常,多線程可能服務多個會話。這種情況下,Dictionary集合需要被一個線程感覺的類包裹來處理“競争條件”。

3) 資源釋放(支援Idisposable接口)

這意味着什麼?萬一你存儲了一個不受托管的資源在MultiMap集合中,它能夠支援釋放存儲的這些資源通過一個調用。難道你不願意寫multiMapObject.Dispose()?而是願意再枚舉每一項的使用調用Dispose嗎?

補充:本文給我們提供了一個很到的思路——多線程之間的并發問題,以及.net内置類型的組合問題。你甚至可以做這樣的組合Dictionary<Key,

Dictionary<Key,Value>>,來滿足你特定場景的存儲邏輯。當然,如果支援多線程,那麼并發問題,還是得考慮地比較周全。

引用:

<a href="http://blogs.msdn.com/pfxteam/archive/2008/08/12/8852005.aspx?CommentPosted=true#commentmessage" target="_blank">http://blogs.msdn.com/pfxteam/archive/2008/08/12/8852005.aspx?CommentPosted=true#commentmessage</a>

<a href="http://blogs.msdn.com/jaredpar/archive/2009/02/11/why-are-thread-safe-collections-so-hard.aspx" target="_blank">http://blogs.msdn.com/jaredpar/archive/2009/02/11/why-are-thread-safe-collections-so-hard.aspx</a>

原文連結:

<a href="http://www.codeproject.com/KB/cs/MultiKeyDictionary.aspx" target="_blank">http://www.codeproject.com/KB/cs/MultiKeyDictionary.aspx</a>

<a href="http://www.codeproject.com/KB/collections/MultiMap_P_2.aspx" target="_blank">http://www.codeproject.com/KB/collections/MultiMap_P_2.aspx</a>

示例源碼和DLL下載下傳位址:

<a href="http://download.csdn.net/detail/yanghua_kobe/3635173" target="_blank">http://download.csdn.net/detail/yanghua_kobe/3635173</a>

原文釋出時間為:2011-09-25

繼續閱讀