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