.NET 4.0 多線程開發系列之
對象的延遲創建與多線程安全訪問
=========================
版權聲明:
本文作者金旭亮擁有此文的原創版權,任何人均可以出于學習與交流目的在網絡中共享與傳播此文,但不得用于商業目的,比如用于出版技術書籍或者進行以盈利為目的的商業培訓。
另外,如有轉貼請注明出處。
有培訓需求的單位請直接與本人聯系。
此聲明適用于本人在互聯網上發表的所有原創類型文章和相關的技術與教學資源。
====================================
1 使用多線程延遲創建“唯一”的對象
在實際開發中,我們可能會希望將一個對象的創建延遲到需要真正用到它的時候。最典型的是使用數據庫連接對象訪問數據庫。在分布式的軟件系統中,客戶端與服務器一般不會在同一臺計算機上,創建數據庫對象并啟動到遠程數據庫連接是一件比較耗廢系統資源的事情。當然,通過編寫一些條件判斷語句我們可以實現“在需要的時候才創建對象”這一目標。
以下是一段典型代碼 :
class Parent
{
A obj = null ;
public void VisitEmbedObject()
{
if (obj == null )
obj = new A ();
}
}
然而,當延遲動態創建的對象會被多線程共享訪問時,就麻煩了,想想上述代碼放在線程函數中,由多個線程同時執行,如果不施加任何的同步手段, A 對象可能會被創建多個!這很容易理解,由于操作系統采用分時時間片的調度方法將 CPU 分配給特定線程執行,因此完全可能發生某個線程還沒有執行完畢,另一個線程又投入運行的情況。
在本例中,有可能一個線程在創建對象過程中(而此時 obj 仍是 null ),另一個線程嘗試訪問 obj 會發現它仍是 null ,于是又會創建另一個 A 對象!
在過去,為了解決這個問題,一般需要給多線程共享資源加鎖:
class Parent
{
A obj = null ;
public void VisitEmbedObject()
{
lock ( this )
{
if (obj == null )
obj = new A ();
}
//...
}
}
這個方法是傳統的編程方法。
然而,到了 .NET 4.0 ,有更簡單更方便的方法達到同樣的目的。
2 泛型類 Lazy<T>
泛型類 Lazy<T> 位于 System 命名空間,是 .NET 4.0 新引入的。它的功能就是解決多線程運行環境下的對象延遲創建問題。
通過實例可以很清楚地掌握它的用法( Demo : UseLazyExample )。
本例中我們定義了一個很簡單的類型 A :
class A
{
public A()
{
Console.WriteLine("A 對象創建,其標識: {0}",this.GetHashCode());
}
public int IntValue
{
get; set;
}
}
以下代碼實現了對象的延遲創建:
class Program
{
static void Main(string[] args)
{
Lazy<A> AObj = new Lazy<A>();
Console.WriteLine(" 現在將給 A 對象的 IntValue 屬性賦值 100");
A obj = AObj.Value; // 此處導致對象創建!
obj.IntValue = 100;
Console.WriteLine("A 對象 {0} 的 IntValue 屬性 ={1}",
obj.GetHashCode(),obj.IntValue);
Console.ReadKey();
}
}
運行結果如下:
<!-- [if gte vml 1]><v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f"> <v:stroke joinstyle="miter" /> <v:formulas> <v:f eqn="if lineDrawn pixelLineWidth 0" /> <v:f eqn="sum @0 1 0" /> <v:f eqn="sum 0 0 @1" /> <v:f eqn="prod @2 1 2" /> <v:f eqn="prod @3 21600 pixelWidth" /> <v:f eqn="prod @3 21600 pixelHeight" /> <v:f eqn="sum @0 0 1" /> <v:f eqn="prod @6 1 2" /> <v:f eqn="prod @7 21600 pixelWidth" /> <v:f eqn="sum @8 21600 0" /> <v:f eqn="prod @7 21600 pixelHeight" /> <v:f eqn="sum @10 21600 0" /> </v:formulas> <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect" /> <o:lock v:ext="edit" aspectratio="t" /> </v:shapetype><v:shape id="圖片_x0020_1" o:spid="_x0000_i1028" type="#_x0000_t75" style='width:267.75pt;height:92.25pt;visibility:visible;mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
上述代碼雖然實現了對象的延遲創建,但示例代碼運行于單線程環境下,還沒有顯示出使用 泛型類 Lazy<T> 的好處。
3 多線程環境下使用泛型類 Lazy<T>
考慮一下新的編程場景:
現在有多個線程都在運行中,這些線程都需要調用 A 類型的對象所提供的功能。我們希望只創建一個 A 對象并且能讓多個線程安全地訪問它。
請看示例 UseLazyInMultiThreadEnvironment 。這是一控制臺程序,以下代碼位于 Program 類中。
首先定義一個用于多線程共享的對象 AObj ,注意它使用 Lazy<T> 進行了封裝:
static Lazy < A > AObj = null ;
緊接著定義一個用于創建 A 對象的工廠函數:
static Func < A > valueFactory = delegate ()
{
Console .WriteLine( " 調用工廠函數創建 A 對象 " );
A obj = new A { IntValue = ( new Random ()).Next(1,100) };
return obj;
};
注意上面用到了 C# 中的匿名方法實現給委托變量賦值。之所以將這個方法稱為“工廠函數”,來自于《設計模式》一書中的“抽象類工廠”設計模式,簡言之,可將負責創建特定類型對象的函數稱為“工廠”,創建出來的對象就是這個工廠的“產品”。
緊接著是一個線程函數,將被多個線程同時執行:
static void ThreadFunc()
{
Console.WriteLine(" 對象 {0} 的 IntValue={1}",
AObj.Value .GetHashCode(), AObj.Value .IntValue);
}
注意:上面訪問共享對象是通過 Lazy<A> 進行的,這是實現多線程同步的關鍵所在。
好了,以下是實驗代碼:
static void Main(string[] args)
{
Console.WriteLine("/n 敲任意鍵開始演示, ESC 退出 ...");
while (Console.ReadKey(true).Key != ConsoleKey.Escape)
{
Console.WriteLine();
// 注意將第 2 個參數改為不同的值:
//1 NotThreadSafe
//2 AllowMultipleThreadSafeExecution
//3 EnsureSingleThreadSafeExecution
// 運行看看結果有何不同?
AObj = new Lazy<A>(valueFactory, LazyExecutionMode.EnsureSingleThreadSafeExecution);
for (int i = 0; i < 10; i++)
{
Thread th = new Thread(ThreadFunc);
th.Start();
}
}
}
}
上述代碼運行時,敲任意鍵將創建 10 個線程,這 10 個線程將嘗試訪問同一個 A 類型的對象。
這里要特別注意 Lazy<A> 的構造函數,它的第一個參數表示當創建共享對象時要調用的工廠函數,第二個參數對程序的執行有著重大影響。以下是部分實驗結果:
LazyExecutionMode. NotThreadSafe
<!-- [if gte vml 1]><v:shape id="圖片_x0020_4" o:spid="_x0000_i1027" type="#_x0000_t75" style='width:243.75pt;height:284.25pt;visibility:visible; mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image003.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image003.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
可以看到, 10 個線程運行時創建了 3 個對象,不同線程得到的值可能相同也可能不同,而且程序執行時對象創建的次數還會有變化。
LazyExecutionMode. AllowMultipleThreadSafeExecution
<!-- [if gte vml 1]><v:shape id="圖片_x0020_7" o:spid="_x0000_i1026" type="#_x0000_t75" style='width:237.75pt; height:260.25pt;visibility:visible;mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image005.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image005.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
可以看到,雖然在這種情況下 A 對象也創建了多個,但多個線程最終訪問的卻是同一個對象,這個同步是由 Lazy<A> 實現的。
LazyExecutionMode. EnsureSingleThreadSafeExecution
<!-- [if gte vml 1]><v:shape id="圖片_x0020_10" o:spid="_x0000_i1025" type="#_x0000_t75" style='width:213.75pt; height:236.25pt;visibility:visible;mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image007.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image007.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
可以看到,不管有多個線程, Lazy<A> 將保證只調用工廠函數一次,僅創建一個 A 對象。
注意:
Lazy<T>僅能保證多線程訪問的是同一個對象,但這并不是說Lazy<T>能自動同步對此共享對象的訪問。這意味著您必須在線程函數中使用lock等線程同步手段避免“多線程訪問共享資源導致數據存取錯誤”現象的發生。
4 小結:
.NET 4.0 是 .NET 歷史上一個重要的版本,引入了不少新的技術,同時對原有的組件也進行了更新,在后面的系列文章中,我將帶領大家再去探索 .NET 4.0 多線程開發的另外一些實用的開發技巧。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
