延遲初始化

延遲初始化

一個對象的延遲初始化意味着該對象的建立將會延遲至第一次使用該對象時。 (在本主題中,術語「延遲初始化」和「延遲實例化」是同義詞。)延遲初始化主要用於提升性能,避免浪費計算,並減小程序內存要求。 如下是最多見的方案:ios

  • 有一個對象的建立開銷很大,而程序可能不會使用它。 例如,假定您在內存中有一個 Customer 對象,該對象的 Orders 屬性包含一個很大的 Order 對象數組,該數組須要數據庫鏈接以進行初始化。 若是用戶從未要求顯示 Orders 或在計算中使用其數據,則沒有理由使用系統內存或計算週期來建立它。 經過使用 Lazy<Orders>Orders 對象聲明爲延遲初始化,能夠避免在不使用該對象的狀況下浪費系統資源。數據庫

  • 有一個對象的建立開銷很大,您想要將建立它的時間延遲到完成其餘開銷大的操做以後。 例如,假定您的程序在啓動時加載若干個對象實例,但只有一些對象實例須要當即執行。 經過將沒必要要的對象的初始化延遲到已建立必要的對象以後,能夠提升程序的啓動性能。express

儘管您能夠編寫本身的代碼來執行延遲初始化,但咱們推薦使用 Lazy<T> Lazy<T> 及其相關的類型還支持線程安全,並提供一致的異常傳播策略。數組

下表列出了 .NET Framework 版本 4 提供的、可在不一樣方案中啓用延遲初始化的類型。緩存

類型安全

說明多線程

[ T:System.Lazy`1 ]app

一個包裝類,可爲任意類庫或用戶定義的類型提供延遲初始化語義。less

[ T:System.Threading.ThreadLocal`1 ]ide

相似於 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> 構造函數具備一個名爲 modeLazyThreadSafetyMode 參數。 這些構造函數提供一個額外的線程安全性模式。 下表顯示指定線程安全性的構造函數參數如何影響 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.NoneLazyThreadSafetyMode.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)()

(ExecutionAndPublication)

Lazy(T)(Func(T))

(ExecutionAndPublication)

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];
相關文章
相關標籤/搜索