您能看出这个Double Check里的问题吗?(解答)
問題請參考:您能看出這個Double Check里的問題嗎?
已經很有很多朋友得到了結果,是由于m_categories過早初始化,而導致double check的驗證條件被破壞(或者說,滿足)。
private object m_mutex = new object(); private Dictionary<int, Category> m_categories;public Category GetCategory(int id) {if (this.m_categories == null){lock (this.m_mutex){if (this.m_categories == null){LoadCategories();}}}return this.m_categories[id]; }private void LoadCategories() {this.m_categories = new Dictionary<int,Category>();this.Fill(GetCategoryRoots()); }private void Fill(IEnumerable<Category> categories) {foreach (var cat in categories){this.m_categories.Add(cat.CategoryID, cat);Fill(cat.Children);} }假設第一個線程進入了GetCategory方法,它自然可以暢通無阻地執行LoadCategories。只可惜,在LoadCategories方法的第一行就為m_categories設置了一個空字典。如果現在立即有另一個線程訪問了GetCategory方法,就會發現m_categories字段不是null,并直接執行this.m_categories[id]這行代碼——但此時,第一個線程還沒有將這個字典填充完畢!
因此,這段代碼其實是一個有問題的Double Check實現。那么我們該怎么改呢?
一位匿名朋友提出,可以增加一個標記,用來表示有沒有初始化完畢。如下:
private bool m_initialized = false;public Category GetCategory(int id) {if (!this.m_initialized){lock (this.m_mutex){if (!this.m_initialized){LoadCategories();this.m_initialized = true;}}}return this.m_categories[id]; }這是個非常漂亮的做法,完全沒有問題。不過我并沒有使用這種修改方式。
private void LoadCategories() {var categories = new Dictionary<int,Category>();Fill(categories, GetCategoryRoots());this.m_categories = categories; }private static void Fill(Dictionary<int, Category> container, IEnumerable<Category> categories) {foreach (var cat in categories){container.Add(cat.CategoryID, cat);Fill(container, cat.Children);} }我稍稍改變了一下Fill方法,它不再直接訪問m_categories字段,而是把內容填充至container參數中。而在LoadCategories方法中,我們創建一個字典,但是直到填充完畢后才將其賦給m_categories字段。這樣就保證了在m_categories字段不為null的時候,一定已經初始化完畢了。這也是一種可行的辦法。我沒有使用第一種做法的原因,并不是因為所謂的“節省空間”,而是……一下子就想到了第二種做法。:)
這里反映了Double Check在使用時的一個準則:在滿足if條件的時候,一定要確保所有的初始化已經完成了。或者說,一定要將“滿足if條件”的操作放在初始化完畢之后進行。至于是否使用某個標記,倒不是什么大問題。
如果您使用.NET編寫代碼,目前已經沒有問題了,但是在某些情況下這樣的代碼還是會出現問題。我認為這也是多線程編程時最麻煩的地方——就是所謂的“Memory Consistency Model”。
為了性能考慮,編譯器在將文本代碼轉化為機器碼,以及CPU在執行機器碼時都會對執行進行“重新排序(reorder)”,reorder的作用是為了提升性能。雖然從單線程的角度來看,reorder不會形成問題,但是在多線程的環境中,reorder就會破壞代碼的邏輯了。如果沒有一個“標準”在進行統一的話,不同的編譯器,虛擬機,CPU架構都會有不同的reorder策略。例如微軟并行庫之父Joe Duffy在這篇文章中簡單地提到了不同平臺(JVM / CLR 2.0)或不同CPU架構(x86 / IA64)下reorder規則的區分。
而臭名昭著的Double Check的bug便是由于store reordering造成的。在JVM或普通的C、C++中并不保證store reordering不會發生。也就是說,您在代碼中看到的兩個變量的“設置”順序,并不代表CPU在執行的時候,也是同樣的效果。因此,如果你觀察下面的代碼:
class Foo { private Helper helper = null;public Helper getHelper() {if (helper == null) synchronized(this) {if (helper == null) helper = new Helper();}return helper;} }看上去這是一段再正常不過的實現Double Check的Java代碼,但是由于發生了store reordering,可能在Helper構造函數中的操作還沒有全部執行完成之前,就設置了helper字段。因此另一個線程就可能會訪問到一個沒有初始化完整的Helper對象。如果您對這個話題感興趣,可以參考《The "Double-Checked Locking is Broken" Declaration》。
而在CLR 2.0中,只會發生load reordering,而不會出現store reordering。于是.NET中編寫的Double Check代碼不會出現任何問題。那么CLR是如何保證在不同的CPU平臺上出現相同的行為呢?那是因為CLR會根據不同的平臺,在合適的情況下插入一些輔助代碼(如Memory Barrier),可見CLR為我們的并行編程環境已經形成了一個相對比較方便的平臺了——雖然,并行編程還是很困難。
(似乎關于Memory Model的有些說法不太確切,隨時更新,希望了解這些的朋友們也可以提點意見,我晚上回家后再查些資料)
from:?http://blog.zhaojie.me/2009/09/double-check-failure-answer.html
總結
以上是生活随笔為你收集整理的您能看出这个Double Check里的问题吗?(解答)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 您能看出这个Double Check里的
- 下一篇: 我犯了一个错误,您能指出吗?