在 dotnet 的最佳實踐里面,不推薦在靜態建構式里面包含復雜的邏輯,其中也就包含了本文聊的和多執行緒相關的鎖的使用,最佳做法是盡量不要在靜態建構式里面碰到任何和鎖以及多執行緒安全相關的邏輯,本文來告訴大家,在靜態建構式里面使用鎖將帶來的問題以及原因
在 .NET 的設計里面,一個型別的靜態建構式,是在此型別第一次被碰到時將會被 CLR 呼叫,呼叫的時候,只允許一個執行緒執行進入靜態建構式,換句話說是一個型別的靜態建構式不會重復被多個執行緒執行,只會被執行一次,如此即可保證靜態建構式的安全性
不同于實體建構式,實體建構式大部分由代碼里面的 new 關鍵詞觸發,執行代碼的僅有一個執行緒,如果多個執行緒呼叫 new 關鍵詞那么將創建出來不同的實體,分別參考不同的記憶體空間,如以下代碼
var foo = new Foo();
如果有多個執行緒同時進入,呼叫到 new Foo() 這句代碼,自然是創建出多個不同的實體,這就意味著無論是靜態建構式還是實體建構式,都是只能被一個執行緒執行,當然,這是有例外的,由于在 .NET 里面,無論是靜態建構式還是實體建構式,都是一個函式方法,通過反射,依然可以當成基礎的方法呼叫,因此在使用反射時,以上的說法是不成立的
在不使用反射的黑科技下,保持讓建構式只能由一個執行緒執行,可以解決十分多的執行緒同步安全問題
對于實體的建構式只能由一個執行緒執行這個十分好理解,由于進入代碼里面,不同的執行緒將會創建出不同的物件,每個物件都有自己的獨立的記憶體空間,獨立的記憶體空間里面執行的實體建構式執行的程序引數以及欄位等都是獨立的,實際有兩個執行緒同時呼叫 new Foo() 代碼,兩個執行緒所使用的實體建構式也是不同的,例如建構式里面使用的程序引數 this. 的 this 就分別屬于不同的兩個物件
然而靜態建構式就比較復雜起來的,大家都知道,在沒有標記執行緒靜態的前提下,所有的靜態欄位和屬性等都是全域共享的,全域共享的就意味著所有的執行緒都訪問到的相同的物件
如上文所說,一個型別的靜態建構式將在型別第一次被碰到時被 CLR 呼叫,那如何了解當前是第一次碰到?如果有兩個執行緒同時都碰到呢,此時由哪個執行緒執行,還是兩個執行緒都要執行?
在靜態建構式被多個執行緒碰到時,相當于進入了資源競爭,無論是多少個執行緒同時碰到某個型別,此型別的靜態建構式只能由其中的一個執行緒執行,而其他執行緒進入等待程序,相當于進入靜態建構式時設定了一個鎖物件,只有一個執行緒能進入呼叫靜態建構式,其他執行緒只能等待靜態建構式執行完成才能繼續
多執行緒在碰到某個型別的靜態建構式時,就和碰到競態資源一樣,也相當于碰到一個鎖
然而靜態建構式的多執行緒安全問題可比其他的競態資源更加復雜,原因也如上文描述,一個型別的靜態建構式是在這個型別第一次被碰到的時候觸發,然而代碼里面什么時候是第一次碰到,這個是非常復雜且不可控的,而且也會隨著代碼的迭代而被變更的,例如當前是十分確定有某個函式碰到了某個型別,然而很快就會因為函式之前的呼叫順序變更,從而變更了靜態建構式的初始化時機,或者在代碼迭代時,在新的時機更快碰到了某個型別,從而觸發了型別的靜態建構式
沒有開發者會在寫代碼的時候,想到碰到某個型別時,需要關注此型別的靜態建構式的初始化時機是否被更改,從而導致了問題,如果真的如此關注了,那代碼也寫不了了,碰到的每一個型別,都需要關注一下的話,這個開發就不好玩了
這就是為什么最佳實踐里面推薦不要在靜態建構式里面放復雜的邏輯,推薦只是做一些簡單的初始化邏輯,如此能很大解決因為靜態建構式的時機問題導致的問題,無論什么時候碰到靜態建構式,如果靜態建構式只是做非常簡單的和無依賴的邏輯,那自然是沒有什么問題
而如果是如本文要聊的,在型別的靜態建構式里面,碰到了鎖,那這個故事就開始復雜起來了
無論是什么語言,只要還是在圖靈的體系下,只要在玩多執行緒,那么鎖和原子和事務是少不了的,不過這是一個很大的話題,本文只來和大家聊鎖與靜態建構式,在使用鎖的時候,能帶來的優勢是提供了一個解決多執行緒安全問題的方法,帶來的問題是多執行緒安全問題,沒錯鎖是一個會導致的執行緒安全問題的解決多執行緒問題的方法,是否會導致問題,完全取決于如何使用,鎖不是一個完美的解決方案,如果使用不當,那帶來的執行緒安全問題將會有很多,而且鎖的使用注意點也非常多,這就是為什么會有本文的核心原因
在使用鎖的最佳實踐里面,就有確定性的說法,也就是說何時捕獲鎖、等待鎖,以及合適釋放鎖都應該是確定的,而不能是不確定的行為,否則輕的話就是執行緒不安全,資源被意外搶入,重的話就是無限執行緒互等,應用進入摸魚狀態,啥都不做都在等著鎖,或者應用拉滿了計算資源瘋狂執行
在靜態建構式里面使用鎖將違背鎖的最佳實踐里面的確定性呼叫這一條,靜態建構式是在型別第一次碰到時被觸發,也就是開發者是無法確定靜態建構式合適被呼叫的,再加上一些代碼優化和行內,將會導致除錯下和發布下的行為也會不同,再加上代碼迭代,靜態建構式的觸發時機也是很難進行控制的,在靜態建構式里面使用鎖將是一個危險的行為,即使當前版本在除錯下是能符合預期作業的,然而在發布的時候,在某些用戶的設備上,也許就會遇到奇怪的問題,如果想要提升產品的代碼質量,就需要盡量不要在靜態建構式里面使用鎖的相關方法,包括直接或間接的呼叫到鎖
舉一個例子來告訴大家在靜態建構式里面呼叫鎖的相關方法導致的多執行緒互等的問題
假設在 Foo 型別的靜態建構式里面需要使用到一個叫 LockObject 物件的鎖,而這個 LockObject 物件的鎖是有多個型別在呼叫的,定義代碼如下
class Foo2
{
public static void Do(Action action)
{
lock (LockObject)
{
action();
}
}
public static readonly object LockObject = new object();
}
此時有 Foo1 型別,在靜態建構式呼叫了 Foo2 的 Do 方法,代碼如下
class Foo1
{
static Foo1()
{
Foo2.Do(() =>
{
// 忽略代碼
Number = 0;
});
}
public static int Number { get; private set; }
}
以上代碼在 Foo1 被第一次碰到的程序中,可能會存在多執行緒相互等待,例如呼叫代碼如下
var task1 = Task.Run(() =>
{
Foo2.Do(() =>
{
Thread.Sleep(2000);
GetFoo1Number();
});
});
var task2 = Task.Run(() =>
{
GetFoo1Number();
});
private static int GetFoo1Number()
{
return Foo1.Number;
}
運行代碼可以看到 task1 和 task2 在互等,點擊暫停,可以看到 task1 和 task2 對應的執行緒的執行緒號分別是 9764 和 22044 兩個,其呼叫堆疊分別如下
執行緒號是 9764 的 task1 的呼叫堆疊如下
> Demo.dll!Demo.Foo1.Number.get() 行 67 C#
Demo.dll!Demo.MainWindow.GetFoo1Number() 行 51 C#
Demo.dll!Demo.MainWindow..ctor.AnonymousMethod__0_2() 行 35 C#
Demo.dll!Demo.Foo2.Do(System.Action action) 行 76 C#
執行緒號是 22044 的 task2 的呼叫堆疊如下
[正在等待執行緒 鎖定 擁有的 9764,雙擊或按 Enter 可切換到執行緒]
System.Private.CoreLib.dll!System.Threading.Monitor.Enter(object obj, ref bool lockTaken) 未知
> Demo.dll!Demo.Foo2.Do(System.Action action) 行 74 C#
Demo.dll!Demo.Foo1.Foo1() 行 60 C#
[本機到托管的轉換]
[托管到本機的轉換]
Demo.dll!Demo.Foo1.Number.get() 行 67 C#
也就是說 task1 在嘗試拿到 Foo1 的 Number 屬性,需要先等待 Foo1 的靜態建構式執行完成,然而 Foo1 的靜態建構式是在 task2 對應的執行緒執行,而 Foo1 的靜態建構式碰到的 Foo2 的 LockObject 物件的鎖被 task1 對應的執行緒獲取,因此想要讓 Foo1 的靜態建構式能繼續執行,就需要等待 task1 執行緒釋放鎖物件,然而 task1 要釋放鎖物件的前提是能獲取完成 Foo1 的 Number 屬性,但是獲取 Foo1 的 Number 屬性需要等待在 task2 上執行的 Foo1 的靜態建構式執行完成
也就是說在 task1 上執行的代碼,需要等待 task2 執行完成,才能釋放鎖,在 task2 上執行的代碼,需要等待 task1 釋放鎖才能執行完成,完美讓兩個執行緒進入互等
這就是其中的一個執行緒不安全的例子,如果將 task1 里面的 Thread.Sleep 去掉,那才是可怕,因為運行代碼,將會發現有時存在執行緒互等,有時不存在,如果這是發給用戶端執行的應用,那將會有用戶反饋說為什么有時候應用就啥也不干了,但有時又跑得好好的,說不定這時客服小姐姐的重啟搞定一切的大法就能解決這個問題,但是如果剛好是大佬用戶遇到了,要求開發者一定要解決,那預計開發者想要復現這個問題,也是很不好玩的,如果進入以上方法的步驟比較多,那大概可以連續多加幾天的班,如果再加上邏輯稍微復雜,加班的時候自己不清醒,那預計還是解決不了的
保持靜態建構式的簡單,可以解決大量的問題,不要在靜態建構式里面添加復雜的代碼,如果真的有這個需求,將這些復雜的代碼放在一個靜態函數里面,自己尋找合適的時機呼叫
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/508703.html
標籤:.NET技术
