延遲初始化
一個對象的延遲初始化意味着該對象的創建將會延遲至第一次使用該對象時。 (在本主題中,術語“延遲初始化”和“延遲實例化”是同義詞。)延遲初始化主要用於提高性能,避免浪費計算,並減少程序內存要求。 以下是最常見的方案:
-
有一個對象的創建開銷很大,而程序可能不會使用它。 例如,假定您在內存中有一個 Customer 對象,該對象的 Orders 屬性包含一個很大的 Order 對象數組,該數組需要數據庫連接以進行初始化。 如果用戶從未要求顯示 Orders 或在計算中使用其數據,則沒有理由使用系統內存或計算周期來創建它。 通過使用 Lazy<Orders> 將 Orders 對象聲明為延遲初始化,可以避免在不使用該對象的情況下浪費系統資源。
-
有一個對象的創建開銷很大,您想要將創建它的時間延遲到完成其他開銷大的操作之后。 例如,假定您的程序在啟動時加載若干個對象實例,但只有一些對象實例需要立即執行。 通過將不必要的對象的初始化延遲到已創建必要的對象之后,可以提高程序的啟動性能。
盡管您可以編寫自己的代碼來執行延遲初始化,但我們推薦使用 Lazy<T>。 Lazy<T> 及其相關的類型還支持線程安全,並提供一致的異常傳播策略。
下表列出了 .NET Framework 版本 4 提供的、可在不同方案中啟用延遲初始化的類型。
類型 |
說明 |
---|---|
[ T:System.Lazy`1 ] |
一個包裝類,可為任意類庫或用戶定義的類型提供延遲初始化語義。 |
[ T:System.Threading.ThreadLocal`1 ] |
類似於 Lazy<T>,只不過它基於本地線程提供延遲初始化語義。 每個線程都可以訪問自己的唯一值。 |
[ T:System.Threading.LazyInitializer ] |
為對象的延遲初始化提供高級的 static(Visual Basic 中為 Shared)方法,此方法不需要類開銷。 |
基本的延遲初始化
若要定義延遲初始化的類型(例如,MyType),請使用 Lazy<MyType>(Visual Basic 中為 Lazy(Of MyType)),如以下示例中所示。 如果在 Lazy<T> 構造函數中沒有傳遞委托,則在第一次訪問值屬性時,將通過使用 Activator.CreateInstance 來創建包裝類型。 如果該類型沒有默認的構造函數,則引發運行時異常。
在以下示例中,假定 Orders 是一個類,該類包含從數據庫檢索的 Order 對象的數組。 Customer 對象包含一個 Orders 實例,但根據用戶操作,可能不需要來自 Orders 對象的數據。
// Initialize by using default Lazy<T> constructor. The // Orders array itself is not created yet. Lazy<Orders> _orders = new Lazy<Orders>();
此外,還可以在 Lazy<T> 構造函數中傳遞一個委托,用於在創建時調用包裝類的特定構造函數重載,並執行所需的任何其他初始化步驟,如以下示例中所示。
// Initialize by invoking a specific constructor on Order when Value // property is accessed Lazy<Orders> _orders = new Lazy<Orders>(() => new Orders(100));
在創建延遲對象之后,在第一次訪問延遲變量的 Value 屬性之前,將不會創建 Orders 的實例。 在第一次訪問包裝類型時,將會創建並返回該包裝類型,並將其存儲起來以備任何將來的訪問。
// We need to create the array only if displayOrders is true if (displayOrders == true) { DisplayOrders(_orders.Value.OrderData); } else { // Don't waste resources getting order data. }
Lazy<T> 對象始終返回初始化時使用的相同對象或值。 因此,Value 屬性是只讀的。 如果 Value 存儲引用類型,則不能為它分配新對象。 (但是,可以更改其可設置的公共字段和屬性的值。)如果 Value 存儲一個值類型,則不能修改它的值。 但是,可以使用新的參數通過再次調用變量構造函數來創建新的變量。
_orders = new Lazy<Orders>(() => new Orders(10));
在第一次訪問 Value 屬性之前,新的延遲實例(與早期的延遲實例類似)不會實例化 Orders。
線程安全初始化
默認情況下,Lazy<T> 對象是線程安全的。 這意味着如果構造函數未指定線程安全性的類型,它創建的 Lazy<T> 對象都是線程安全的。 在多線程方案中,要訪問線程安全的 Lazy<T> 對象的 Value 屬性的第一個線程將為所有線程上的所有后續訪問初始化該對象,並且所有線程都共享相同數據。 因此,由哪個線程初始化對象並不重要,爭用條件將是良性的。
注意 |
---|
您可以使用異常緩存將此一致性擴展至錯誤條件。 有關更多信息,請參見下一節延遲對象中的異常。 |
下面的示例演示了同一個 Lazy<int> 實例對於三個不同的線程具有相同的值。
// Initialize the integer to the managed thread id of the // first thread that accesses the Value property. Lazy<int> number = new Lazy<int>(() => Thread.CurrentThread.ManagedThreadId); Thread t1 = new Thread(() => Console.WriteLine("number on t1 = {0} ThreadID = {1}", number.Value, Thread.CurrentThread.ManagedThreadId)); t1.Start(); Thread t2 = new Thread(() => Console.WriteLine("number on t2 = {0} ThreadID = {1}", number.Value, Thread.CurrentThread.ManagedThreadId)); t2.Start(); Thread t3 = new Thread(() => Console.WriteLine("number on t3 = {0} ThreadID = {1}", number.Value, Thread.CurrentThread.ManagedThreadId)); t3.Start(); // Ensure that thread IDs are not recycled if the // first thread completes before the last one starts. t1.Join(); t2.Join(); t3.Join(); /* Sample Output: number on t1 = 11 ThreadID = 11 number on t3 = 11 ThreadID = 13 number on t2 = 11 ThreadID = 12 Press any key to exit. */
如果在每個線程上需要不同的數據,請使用 ThreadLocal<T> 類型,如本主題后面所述。
一些 Lazy<T> 構造函數具有一個名為 isThreadSafe 的布爾參數,該參數用於指定是否將從多個線程訪問 Value 屬性。 如果您打算只從一個線程訪問該屬性,請傳入 false 以獲得適度的性能好處。 如果您打算從多個線程訪問該屬性,請傳入 true 以指示 Lazy<T> 實例正確處理爭用條件(在此條件下,一個線程將在初始化時引發一個異常)。
一些 Lazy<T> 構造函數具有一個名為 mode 的 LazyThreadSafetyMode 參數。 這些構造函數提供一個額外的線程安全性模式。 下表顯示指定線程安全性的構造函數參數如何影響 Lazy<T> 對象的線程安全性。 每個構造函數最多具有一個這樣的參數:
對象的線程安全性 |
LazyThreadSafetyMode mode 參數 |
布爾 isThreadSafe 參數 |
無線程安全性參數 |
---|---|---|---|
線程完全安全;一次只有一個線程嘗試初始化值。 |
[ F:System.Threading.LazyThreadSafetyMode.ExecutionAndPublication ] |
true |
是。 |
線程不安全。 |
[ F:System.Threading.LazyThreadSafetyMode.None ] |
false |
不適用。 |
線程完全安全;線程通過爭用來初始化值。 |
[ F:System.Threading.LazyThreadSafetyMode.PublicationOnly ] |
不適用。 |
不適用。 |
如該表所示,為 mode 參數指定 LazyThreadSafetyMode.ExecutionAndPublication 與為 isThreadSafe 參數指定 true 相同,指定 LazyThreadSafetyMode.None 與指定 false 相同。
指定 LazyThreadSafetyMode.PublicationOnly 允許多個線程嘗試初始化 Lazy<T> 實例。 只有一個線程在爭用中勝出,所有其他線程將接收由勝出線程初始化的值。 如果在初始化期間線程引發異常,則該線程不接收由勝出線程設置的值。 因為不緩存異常,因此訪問 Value 屬性的后續嘗試可能導致成功的初始化。 這與在其他模式中處理異常的方式不同,后者將在下一節中進行說明。 有關更多信息,請參見 LazyThreadSafetyMode 枚舉。
延遲對象中的異常
如上文所述,Lazy<T> 對象始終返回在初始化時使用的相同對象或值,因此,Value 屬性是只讀的。 如果您啟用異常緩存,則此永久性還將擴展至異常行為。 如果某個遲緩初始化的對象啟用了異常緩存,並在首次訪問 Value 屬性時從其初始化方法引發異常,則以后每次嘗試訪問 Value 屬性時都會引發相同的異常。 換句話說,決不會重新調用包裝類型的構造函數,即使在多線程方案中也是如此。 因此,Lazy<T> 對象不能對一次訪問引發異常,而對后續的訪問返回值。
當您使用任何采用初始化方法(valueFactory 參數)的 System.Lazy<T> 構造函數時,會啟用異常緩存;例如,當您使用 Lazy(T)(Func(T)) 構造函數時,會啟用異常緩存。 如果構造函數還采用 LazyThreadSafetyMode 值(mode 參數),請指定 LazyThreadSafetyMode.None 或 LazyThreadSafetyMode.ExecutionAndPublication。 指定初始化方法會為這兩種模式啟用異常緩存。 初始化方法可以非常簡單。 例如,它可以調用 T 的默認構造函數:new Lazy<Contents>(() => new Contents(), mode) (C#) 或 New Lazy(Of Contents)(Function() New Contents()) (Visual Basic)。 如果您使用不指定初始化方法的 System.Lazy<T> 構造函數,則不會緩存 T 默認構造函數引發的異常。 有關更多信息,請參見 LazyThreadSafetyMode。
注意 |
---|
如果您創建了 Lazy<T> 對象,並將其 isThreadSafe 構造函數參數設置為 false 或將 mode 構造函數參數設置為 LazyThreadSafetyMode.None,則必須從單個線程訪問 Lazy<T> 對象或提供您自己的同步。 這適用於對象的所有方面,包括異常緩存。 |
如上一節所述,通過指定 LazyThreadSafetyMode.PublicationOnly 創建的 Lazy<T> 對象處理異常的方式不同。 使用 PublicationOnly,多個線程可以通過爭用來初始化 Lazy<T> 實例。 在這種情況下,不緩存異常,訪問 Value 屬性的嘗試可以繼續下去,直到初始化成功。
下表總結了 Lazy<T> 構造函數控制異常緩存的方式。
構造函數 |
線程安全模式 |
使用初始化方法 |
緩存異常 |
---|---|---|---|
Lazy(T)() |
否 |
否 |
|
Lazy(T)(Func(T)) |
是 |
是 |
|
Lazy(T)(Boolean) |
True (ExecutionAndPublication) 或 false (None) |
否 |
否 |
Lazy(T)(Func(T), Boolean) |
True (ExecutionAndPublication) 或 false (None) |
是 |
是 |
Lazy(T)(LazyThreadSafetyMode) |
用戶指定 |
否 |
否 |
Lazy(T)(Func(T), LazyThreadSafetyMode) |
用戶指定 |
是 |
如果用戶指定 PublicationOnly 則為“否”,否則為“是”。 |
實現延遲初始化屬性
若要通過使用延遲初始化來實現一個公共屬性,請將該屬性的支持字段定義為 Lazy<T>,並從該屬性的 get 訪問器中返回 Value 屬性。
class Customer { private Lazy<Orders> _orders; public string CustomerID {get; private set;} public Customer(string id) { CustomerID = id; _orders = new Lazy<Orders>(() => { // You can specify any additonal // initialization steps here. return new Orders(this.CustomerID); }); } public Orders MyOrders { get { // Orders is created on first access here. return _orders.Value; } } }
Value 屬性是只讀的;因此,公開它的屬性不具有 set 訪問器。 如果需要 Lazy<T> 對象支持的讀/寫屬性,則 set 訪問器必須創建新的 Lazy<T> 對象並將它分配給支持存儲區。 set 訪問器必須創建返回傳給 set 訪問器的新屬性值的 lambda 表達式,並將該表達式傳給新 Lazy<T> 對象的構造函數。 下一次訪問 Value 屬性將導致初始化新的 Lazy<T>,其 Value 屬性此后將返回分配給該屬性的新值。 進行這種復雜的安排是為了保持內置到 Lazy<T> 的多線程保護。 否則,屬性訪問器必須緩存 Value 屬性返回的第一個值並只修改緩存的值,您必須編寫自己的線程安全代碼來完成此工作。 由於 Lazy<T> 對象支持的讀/寫屬性需要更多初始化,性能可能變低。 此外,根據特定的方案,可能需要更大的協調量來避免 setter 和 getter 之間的爭用條件。
線程本地延遲初始化
在某些多線程方案中,可能要為每個線程提供它自己的私有數據。 此類數據稱為“線程本地數據”。 在 .NET Framework 3.5 和更低版本中,可以將 ThreadStatic 特性應用於靜態變量以使其成為線程本地變量。 但是,使用 ThreadStatic 特性會導致細小的錯誤。 例如,即使基本的初始化語句也將導致該變量只在訪問它的第一個線程上進行初始化,如以下示例中所示。
[ThreadStatic] static int counter = 1;
在所有其他線程上,該變量將通過使用默認值(零)來進行初始化。 在 .NET Framework 4 中,作為一種替代方法,可以使用 System.Threading.ThreadLocal<T> 類型創建基於實例的線程本地變量,此變量可通過您提供的 Action<T> 委托在所有線程上進行初始化。 在以下示例中,所有訪問 counter 的線程都會將其起始值看作 1。
ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 1);
ThreadLocal<T> 包裝其對象與 Lazy<T> 非常相似,但存在以下主要差別:
-
通過使用不可從其他線程訪問的線程自己的私有數據,每個線程都可初始化線程本地變量。
-
ThreadLocal<T>.Value 屬性是可讀寫的,可進行任意次數的修改。 這會影響異常傳播,例如,一個 get 操作可能會引發一個異常,但下一個操作可能會成功地初始化該值。
-
如果未提供初始化委托,則 ThreadLocal<T> 將通過使用其包裝類型的默認值對其進行初始化。 就這一點而言,ThreadLocal<T> 與 ThreadStaticAttribute 特性是一致的。
下面的示例演示了訪問 ThreadLocal<int> 實例的每個線程如何獲取自己的唯一的數據副本。
// Initialize the integer to the managed thread id on a per-thread basis. ThreadLocal<int> threadLocalNumber = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId); Thread t4 = new Thread(() => Console.WriteLine("threadLocalNumber on t4 = {0} ThreadID = {1}", threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)); t4.Start(); Thread t5 = new Thread(() => Console.WriteLine("threadLocalNumber on t5 = {0} ThreadID = {1}", threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)); t5.Start(); Thread t6 = new Thread(() => Console.WriteLine("threadLocalNumber on t6 = {0} ThreadID = {1}", threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)); t6.Start(); // Ensure that thread IDs are not recycled if the // first thread completes before the last one starts. t4.Join(); t5.Join(); t6.Join(); /* Sample Output: threadLocalNumber on t4 = 14 ThreadID = 14 threadLocalNumber on t5 = 15 ThreadID = 15 threadLocalNumber on t6 = 16 ThreadID = 16 */
Parallel.For 和 ForEach 中的線程本地變量
當使用 Parallel.For 方法或 Parallel.ForEach 方法以並行方式循環訪問數據源時,可以使用具有對線程本地數據的內置支持的重載。 在這些方法中,可通過使用本地委托來創建、訪問和清理數據來實現線程本地化。 有關更多信息,請參見如何:編寫具有線程本地變量的 Parallel.For 循環和如何:編寫具有線程局部變量的 Parallel.ForEach 循環。
對低開銷方案使用延遲初始化
在必須延遲初始化大量對象的方案中,您可能會認為在 Lazy<T> 中包裝每個對象需要過多的內存或過多的計算資源。 或者,您可能對如何公開延遲初始化有嚴格的要求。 在這種情況下,可以使用 System.Threading.LazyInitializer 類的 static(在 Visual Basic 中為 Shared)方法來延遲初始化每個對象,並且不將這些對象包裝在 Lazy<T> 實例中。
在以下示例中,假定不將整個 Orders 對象包裝在一個 Lazy<T> 對象中,而是在需要的時候延遲初始化單個 Order 對象。
// Assume that _orders contains null values, and // we only need to initialize them if displayOrderInfo is true if(displayOrderInfo == true) { for (int i = 0; i < _orders.Length; i++) { // Lazily initialize the orders without wrapping them in a Lazy<T> LazyInitializer.EnsureInitialized(ref _orders[i], () => { // Returns the value that will be placed in the ref parameter. return GetOrderForIndex(i); }); } }
在此示例中,請注意,在循環的每次迭代中都會調用初始化過程。 在多線程方案中,要調用初始化過程的第一個線程的值將可以由所有線程看到。 后面的線程還將調用初始化過程,但不使用它們的結果。 如果這種潛在的爭用條件是不可接受的,請使用采用一個布爾參數和一個同步對象的 LazyInitializer.EnsureInitialized 重載。
如何:執行對象的延遲初始化
System.Lazy<T> 類簡化了執行對象的延遲初始化和實例化的工作。 通過以延遲方式實例化對象,可避免在根本不需要的情況下必須創建所有的對象,或者可以將對象的初始化延遲到第一次訪問它們的時候。 有關更多信息,請參見延遲初始化。
示例
下面的示例演示如何使用 Lazy<T> 初始化值。 假定延遲變量可能不是必需的,具體取決於將 someCondition 變量設置為 true 或 false 的一些其他代碼。
static bool someCondition = false; //Initializing a value with a big computation, computed in parallel Lazy<int> _data = new Lazy<int>(delegate { return ParallelEnumerable.Range(0, 1000). Select(i => Compute(i)).Aggregate((x,y) => x + y); }, LazyExecutionMode.EnsureSingleThreadSafeExecution); // Do some work that may or may not set someCondition to true. // ... // Initialize the data only if necessary if (someCondition) { if (_data.Value > 100) { Console.WriteLine("Good data"); } }
下面的示例演示如何使用 System.Threading.ThreadLocal<T> 類來初始化僅對當前線程上的當前對象實例可見的類型。
//Initializing a value per thread, per instance ThreadLocal<int[][]> _scratchArrays = new ThreadLocal<int[][]>(InitializeArrays); // . . . static int[][] InitializeArrays () {return new int[][]} // . . . // use the thread-local data int i = 8; int [] tempArr = _scratchArrays.Value[i];