最近公司的項目中發現一個編譯優化導致的bug。同事敘述為“在CPU開啟out-of-order execution優化時,是有bug的”。針對這個問題,比較好的優化方法如下:
private static JobManager self; private static object asyncObj = new object(); public static JobManager Instance { get { if (self == null) { lock (asyncObj) { if (self == null) { // 正確的實現方法應該為: var temp = new JobManager(); Interlocked.Exchange(ref self, temp); self = new JobManager(); } } } return self; } }
這里需要解釋一下:
self = new JobManager()
這句你的本意是為 JobManager 分配內存,調用構造器初始化字段,再將引用賦給 self ,即發布出來讓其他線程可見。但是,那只是你一廂情願的想法,編譯器可能這樣做:為JobManager 分配內存,將引用發布到(賦給)self,再調用構造器。然而,如果在將引用發布給 self 之后,調用構造器之前,另一個線程發現 self 不為 null,便開始使用JobManager對象,這時會發生什么?這個時候對象的構造器還沒有執行結束!這是一個很難追蹤的bug。
從雙檢鎖技術的角度來看,使用
Interlocked.Exchange確實是最好的解決方案。但有兩個問題,它該如何解決?
1.速度是否夠快?
2.如果一個線程池線程在Monitor的線程同步構造上阻塞,線程池會創建另一個線程來保持CPU的“飽和”,而創建一個新線程的代價是很昂貴的,我們該如何避免這樣的情況?
試着跳出“lock+2次if”的框子,我們可以使用
Interlocked.CompareExchange來解決上面的問題。下面是一個示例:
internal sealed class MySingleton { private static MySingleton s_value = null; public static MySingleton GetMySingleton() { if (s_value != null) return s_value; MySingleton temp = new MySingleton(); Interlocked.CompareExchange(ref s_value, temp, null); return s_value; } }
雖然多個線程同時調用GetMySingleton,會創建2個或者更多的MySingleton對象,但沒有被s_value引用的臨時對象會在以后被垃圾回收。大多數應用程序很少會發生同時調用GetMySingleton的情況,所以不太可能出現創建多個MySingleton對象的情況。上述代碼帶來優勢是很明顯的,首先,它的速度是非常快,其次,它永不阻塞線程。這就解決了前面在雙檢鎖技術中提出的問題。
另外,在.net 4.0中提供了2個類型封裝上述兩種模式(雙檢鎖技術、使用
Interlocked.CompareExchange技術):
泛型
System.Lazy類和
System.Threading.LazyInitializer類。下面是2個示例:
public static void Main() { Lazy<string> s = new Lazy<string>(() => DateTime.Now.ToLongTimeString(), LazyThreadSafetyMode.PublicationOnly); Console.WriteLine(s.IsValueCreated); Console.WriteLine(s.Value); Console.WriteLine(s.IsValueCreated); Thread.Sleep(5000); Console.WriteLine(s.Value); Console.WriteLine(DateTime.Now.ToLongTimeString()); }
輸出結果:
public static void Main() { string name = null; LazyInitializer.EnsureInitialized(ref name, () => "Benjamin"); Console.WriteLine(name); LazyInitializer.EnsureInitialized(ref name, () => "Yao"); Console.WriteLine(name); }
輸出結果:
其中枚舉
LazyThreadSafetyMode解釋如下:
public enum LazyThreadSafetyMode { None = 0, //完全沒有線程安全劫持(適合GUI應用程序) PublicationOnly = 1, //使用Interlocked.CompareExchange技術 ExecutionAndPublication = 2, //使用雙檢鎖技術 }