延迟初始化 延迟初始化 如何:执行对象的延迟初始化

以下是最常见的方案:

  • Lazy<Orders>Orders 对象声明为延迟初始化,可以避免在不使用该对象的情况下浪费系统资源。

  • 通过将不必要的对象的初始化延迟到已创建必要的对象之后,可以提高程序的启动性能。

Lazy<T> 及其相关的类型还支持线程安全,并提供一致的异常传播策略。

下表列出了 .NET Framework 版本 4 提供的、可在不同方案中启用延迟初始化的类型。

类型

说明

[ T:System.Lazy`1 ]

一个包装类,可为任意类库或用户定义的类型提供延迟初始化语义。

[ T:System.Threading.ThreadLocal`1 ]

每个线程都可以访问自己的唯一值。

[ T:System.Threading.LazyInitializer ]

Shared)方法,此方法不需要类开销。

基本的延迟初始化

如果该类型没有默认的构造函数,则引发运行时异常。

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));

在第一次访问包装类型时,将会创建并返回该包装类型,并将其存储起来以备任何将来的访问。

// 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.
}

但是,可以使用新的参数通过再次调用变量构造函数来创建新的变量。

_orders = new Lazy<Orders>(() => new Orders(10));

在第一次访问 Value 属性之前,新的延迟实例(与早期的延迟实例类似)不会实例化 Orders

线程安全初始化

因此,由哪个线程初始化对象并不重要,争用条件将是良性的。

注意

有关更多信息,请参见下一节延迟对象中的异常

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> 类型,如本主题后面所述。

true 以指示 Lazy<T> 实例正确处理争用条件(在此条件下,一个线程将在初始化时引发一个异常)。

每个构造函数最多具有一个这样的参数:

对象的线程安全性

mode 参数

isThreadSafe 参数

无线程安全性参数

线程完全安全;一次只有一个线程尝试初始化值。

[ F:System.Threading.LazyThreadSafetyMode.ExecutionAndPublication ]

true

是。

线程不安全。

[ F:System.Threading.LazyThreadSafetyMode.None ]

false

不适用。

线程完全安全;线程通过争用来初始化值。

[ F:System.Threading.LazyThreadSafetyMode.PublicationOnly ]

不适用。

不适用。

false 相同。

有关更多信息,请参见 LazyThreadSafetyMode 枚举。

延迟对象中的异常

因此,Lazy<T> 对象不能对一次访问引发异常,而对后续的访问返回值。

有关更多信息,请参见 LazyThreadSafetyMode

注意

这适用于对象的所有方面,包括异常缓存。

在这种情况下,不缓存异常,访问 Value 属性的尝试可以继续下去,直到初始化成功。

下表总结了 Lazy<T> 构造函数控制异常缓存的方式。

构造函数

线程安全模式

使用初始化方法

缓存异常

Lazy(T)()

(ExecutionAndPublication)

Lazy(T)(Func(T))

(ExecutionAndPublication)

Lazy(T)(Boolean)

false (None)

Lazy(T)(Func(T), Boolean)

false (None)

Lazy(T)(LazyThreadSafetyMode)

用户指定

Lazy(T)(Func(T), LazyThreadSafetyMode)

用户指定

如果用户指定 PublicationOnly 则为“否”,否则为“是”。

实现延迟初始化属性

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;
        }
    }
}

此外,根据特定的方案,可能需要更大的协调量来避免 setter 和 getter 之间的争用条件。

线程本地延迟初始化

例如,即使基本的初始化语句也将导致该变量只在访问它的第一个线程上进行初始化,如以下示例中所示。

[ThreadStatic]
static int counter = 1;

counter 的线程都会将其起始值看作 1。

ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 1);

ThreadLocal<T> 包装其对象与 Lazy<T> 非常相似,但存在以下主要差别:

  • 通过使用不可从其他线程访问的线程自己的私有数据,每个线程都可初始化线程本地变量。

  • get 操作可能会引发一个异常,但下一个操作可能会成功地初始化该值。

  • 就这一点而言,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 中的线程本地变量

对低开销方案使用延迟初始化

在这种情况下,可以使用 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 重载。

如何:执行对象的延迟初始化

有关更多信息,请参见延迟初始化

示例

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];