最近看到這篇文章dotNetDR_的回復,讓我想起一個真實發生的案例,下面就簡單說說這個關於lock引用類型的一個不容易發現的隱藏缺陷。
某類庫中的代碼,封裝了很簡單的一個通用類,用於線程安全地執行某一種類型的特定方法,幾行代碼搞定:
public class ConcurrentObjectExecutor<T> where T : IDisposable, new() { public void Start() { T obj = new T(); lock (obj) { Console.WriteLine(obj.ToString()); //do sth } } }
設計這個類,估計本來主要是針對繼承自特定接口的類型能夠線程安全地執行某一個方法。如果泛型類型T為類(class),則程序運行沒有任何問題,也能保證線程安全。
但是我們知道泛型約束只有繼承自接口和new還遠遠不能保證T就是一個class,結構(struct)也可以繼承接口,也可以new。比如自定義一個結構:
struct OrderMessger : IDisposable { public void Dispose() { } }
下面的代碼可以編譯通過,運行時也不會拋出異常(如果不看上下文,多數調用者估計就這麼讓它過去,很可能成為今後一個潛在的隱藏很深的bug):
var executor = new ConcurrentObjectExecutor<OrderMessger>(); executor.Start();
很顯然,上面的泛型程序看上去是lock了一個結構,也就是鎖定了一個值類型。但是我們知道,lock關鍵字指定的鎖定對象必須是引用類型。上面的示例中,實際情況是將結構實例obj隱式轉換成了一個引用對象實例(也就是裝箱,每次裝箱都生成了一個新的實例),這樣的lock是毫無意義的,因為在多線程的條件下,線程爭用的obj已經不是指定的同一個實例(的引用)。
比較搞笑的是,直接寫下面的代碼,編譯時直接在lock語句上報告有錯誤(編譯時檢測真是幫了大忙,ms為什麼不把泛型檢測搞的更智能些?):
var obj = new OrderMessger(); lock (obj) { }
錯誤 1 “OrderMessger”不是 lock 語句要求的引用類型
解決的方法也很簡單,泛型約束在原來的基礎上再限定必須是class即可。
最後總結下:類庫設計中,越是通用的東西考慮的應用場景越要周到,其中線程安全是非常重要必不可少的一個環節,線程安全實現過程中,如果對多線程同步原語理解不夠深刻,很可能設計出有潛在缺陷的實現,MSDN關於Thread Safe的調用和說明值得類庫開發者深刻學習和借鑒。