延迟初始化

延迟初始化

一个对象的延迟初始化意味着该对象的建立将会延迟至第一次使用该对象时。 (在本主题中,术语“延迟初始化”和“延迟实例化”是同义词。)延迟初始化主要用于提升性能,避免浪费计算,并减小程序内存要求。 如下是最多见的方案: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];
相关文章
相关标签/搜索