限定的な初期化
更新 : 2011 年 3 月
オブジェクトの限定的な初期化とは、初めて使用されるときまでオブジェクトの作成を延期することです (このトピックでは、"限定的な初期化" と "限定的なインスタンス化" という用語を同じ意味で使用しています)。 限定的な初期化は主に、パフォーマンスの向上、無駄な計算の回避、およびプログラムのメモリ要件の軽減を目的として使用されます。 最も一般的なシナリオは次のとおりです。
オブジェクトの作成時の負荷が高く、プログラムではそれを使用しない可能性がある場合。 たとえば、メモリにある Customer オブジェクトの Orders プロパティに Order オブジェクトの大きな配列が格納されていて、このオブジェクトを初期化するにはデータベースに接続する必要があるとします。 Orders のデータを表示したり計算で使用したりすることをユーザーから要求されない限り、そのオブジェクトを作成するためにシステム メモリや計算サイクルを使用する必要はありません。 Orders オブジェクトの宣言で Lazy<Orders> を使用して限定的な初期化を指定すると、オブジェクトが使用されていないときのシステム リソースの浪費を回避できます。
オブジェクトの作成時の負荷が高く、負荷が高い他の操作が完了するまでそのオブジェクトの作成を延期する場合。 たとえば、プログラムの起動時にいくつかのオブジェクト インスタンスが読み込まれても、すぐに必要なものはそのうちの一部だけであるとします。 この場合、必要なオブジェクトが作成されるまで不要なオブジェクトの初期化を延期すると、プログラムの起動時のパフォーマンスを向上させることができます。
限定的な初期化を実行するためのコードは独自に記述することもできますが、代わりに Lazy<T> を使用することをお勧めします。 Lazy<T> とその関連型はスレッド セーフもサポートしており、例外の反映ポリシーにも一貫性があります。
限定的な初期化をさまざまなシナリオで有効にするために用意されている .NET Framework Version 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.
Dim _orders As Lazy(Of Orders) = New Lazy(Of 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
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)(Function() New Orders(100))
// 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 Then
DisplayOrders(_orders.Value.OrderData)
Else
' Don't waste resources getting order data.
End If
// 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(Of Orders)(Function() New Orders(10))
_orders = new Lazy<Orders>(() => new Orders(10));
前の例と同様に、この新しい限定的なインスタンスでは、Value プロパティに初めてアクセスするまで Orders がインスタンス化されません。
スレッド セーフな初期化
既定では、Lazy<T> オブジェクトはスレッド セーフです。 つまり、コンストラクターでスレッド セーフの種類を指定しない場合、作成される Lazy<T> オブジェクトはスレッド セーフです。 マルチスレッドのシナリオでは、スレッド セーフな Lazy<T> オブジェクトの Value プロパティに最初にアクセスしたスレッドが初期化を行います。その後はどのスレッドからもそのデータにアクセスするため、すべてのスレッドで同じデータが共有されます。 したがって、どのスレッドがオブジェクトを初期化したかは問題にならず、競合状態が発生しません。
メモ |
---|
例外のキャッシュを使用する場合、この一貫性はエラー状態にも当てはまります。詳細については、次の「遅延オブジェクトの例外」セクションを参照してください。 |
3 つの異なるスレッドで、同じ値を持つ同じ Lazy<int> インスタンスを使用している例を次に示します。
' Initialize the integer to the managed thread id of the
' first thread that accesses the Value property.
Dim number As Lazy(Of Integer) = New Lazy(Of Integer)(Function()
Return Thread.CurrentThread.ManagedThreadId
End Function)
Dim t1 As New Thread(Sub()
Console.WriteLine("number on t1 = {0} threadID = {1}",
number.Value, Thread.CurrentThread.ManagedThreadId)
End Sub)
t1.Start()
Dim t2 As New Thread(Sub()
Console.WriteLine("number on t2 = {0} threadID = {1}",
number.Value, Thread.CurrentThread.ManagedThreadId)
End Sub)
t2.Start()
Dim t3 As New Thread(Sub()
Console.WriteLine("number on t3 = {0} threadID = {1}",
number.Value, Thread.CurrentThread.ManagedThreadId)
End Sub)
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.
// 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 プロパティに複数のスレッドからアクセスするかどうかを指定するために使用します。 プロパティに 1 つのスレッドからのみアクセスする場合は、少しでもパフォーマンスが向上するように false を渡します。 プロパティに複数のスレッドからアクセスする場合は、true を渡して、初期化時にいずれかのスレッドから例外がスローされたときに競合状態を正しく処理できるように Lazy<T> インスタンスに指示します。
一部の Lazy<T> コンストラクターには、mode という名前の LazyThreadSafetyMode パラメーターがあります。 これらのコンストラクターでは、追加のスレッド セーフ モードが提供されます。 Lazy<T> オブジェクトのスレッド セーフが、スレッド セーフを指定するコンストラクター パラメーターによって受ける影響を次の表に示します。 各コンストラクターには、このようなパラメーターが 1 つのみあります。
オブジェクトのスレッド セーフ |
LazyThreadSafetyMode mode パラメーター |
isThreadSafe ブール値パラメーター |
スレッド セーフ パラメーターなし |
---|---|---|---|
完全なスレッドセーフ。値の初期化を試行するスレッドは一度に 1 つのみです。 |
[ 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> インスタンスの初期化を試行します。 実際に初期化を実行できるスレッドは 1 つだけで、その他のすべてのスレッドは初期化に成功したスレッドによって初期化された値を受け取ります。 初期化時にスレッドで例外がスローされた場合、そのスレッドは成功したスレッドによって設定された値を受け取りません。 例外はキャッシュされないため、後で Value プロパティにアクセスしようとした場合、初期化に成功することがあります。 これは、他のモードでの例外処理方法とは異なります。その点については、次のセクションで説明します。 詳細については、LazyThreadSafetyMode 列挙体の解説を参照してください。
遅延オブジェクトの例外
既に説明したとおり、Lazy<T> オブジェクトは、初期化の際に使用されたものと同じオブジェクトまたは値を常に返すため、Value プロパティは読み取り専用です。 例外のキャッシュを有効にしている場合、この不変性は、例外の動作にも当てはまります。 限定的に初期化されたオブジェクトで、例外のキャッシュが有効になっており、Value プロパティに最初にアクセスしたときにそのオブジェクトの初期化メソッドから例外がスローされた場合、後で Value プロパティにアクセスしようとした場合も常に同じ例外がスローされます。 つまり、マルチスレッドのシナリオでも、ラップされた型のコンストラクターが再度呼び出されることはありません。 したがって、Lazy<T> オブジェクトで、いずれかのアクセスでは例外をスローし、その後のアクセスでは値を返すということはできません。
例外のキャッシュは、初期化メソッド (valueFactory パラメーター) を受け取る System.Lazy<T> コンストラクターを使用すると有効になります。たとえば、Lazy(T)(Func(T)) コンストラクターを使用すると有効になります。 コンストラクターが LazyThreadSafetyMode の値 (mode パラメーター) も受け取る場合は、LazyThreadSafetyMode.None または LazyThreadSafetyMode.ExecutionAndPublication を指定します。 初期化メソッドを指定すると、これら 2 つのモードで例外のキャッシュが有効になります。 初期化メソッドは非常に簡単なものにできます。 たとえば、T の既定のコンストラクター (C# では new Lazy<Contents>(() => new Contents(), mode)、Visual Basic では New Lazy(Of Contents)(Function() New Contents())) を呼び出すことができます。 初期化メソッドを指定しない System.Lazy<T> コンストラクターを使用する場合、T の既定のコンストラクターによってスローされる例外はキャッシュされません。 詳細については、LazyThreadSafetyMode 列挙体の解説を参照してください。
メモ |
---|
isThreadSafe コンストラクターのパラメーターが false に設定されている、または mode コンストラクターのパラメーターが LazyThreadSafetyMode.None に設定されている Lazy<T> オブジェクトを作成する場合は、シングル スレッドから 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> として定義し、Value プロパティをそのプロパティの get アクセサーから返します。
Class Customer
Private _orders As Lazy(Of Orders)
Public Shared CustomerID As String
Public Sub New(ByVal id As String)
CustomerID = id
_orders = New Lazy(Of Orders)(Function()
' You can specify additional
' initialization steps here
Return New Orders(CustomerID)
End Function)
End Sub
Public ReadOnly Property MyOrders As Orders
Get
Return _orders.Value
End Get
End Property
End Class
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 アクセサーに渡された新しいプロパティ値を返すラムダ式を作成し、そのラムダ式を新しい Lazy<T> オブジェクトのコンストラクターに渡す必要があります。 Value プロパティに次にアクセスしたときに、新しい Lazy<T> が初期化されます。その後、その Value プロパティは、そのプロパティに代入された新しい値を返します。 このような複雑な処理を行うのは、Lazy<T> に組み込まれているマルチスレッド保護を維持するためです。 そのようにしないと、プロパティ アクセサーでは Value プロパティから返された最初の値をキャッシュし、キャッシュされた値のみを変更するようになり、マルチスレッド保護を維持するには独自のスレッド セーフなコードを記述することが必要になります。 Lazy<T> オブジェクトでサポートされる読み取り/書き込みプロパティによって必要な初期化処理が追加されるため、パフォーマンスが許容範囲を下回る可能性があります。 さらに、特定のシナリオでは、setter と getter との間の競合状態を回避するために、追加の調整が必要になることがあります。
スレッド ローカルな限定的な初期化
マルチスレッドの一部のシナリオでは、スレッドごとに独自のプライベート データを提供することもできます。 このようなデータのことをスレッド ローカル データと呼びます。 .NET Framework Version 3.5 以前では、ThreadStatic 属性を静的変数に割り当てることでスレッド ローカルにすることができます。 ただし、ThreadStatic 属性を使用すると、明確でないエラーが発生する場合があります。 たとえば、次の例に示すように、基本的な初期化ステートメントでも、最初にアクセスしたスレッドでしか変数が初期化されないことがあります。
<ThreadStatic()>
Shared counter As Integer
[ThreadStatic]
static int counter = 1;
他のすべてのスレッドでは、既定値 (0) を使用して変数が初期化されます。 .NET Framework Version 4 では、代わりに System.Threading.ThreadLocal<T> 型を使用して、指定した Action<T> デリゲートによってすべてのスレッドで初期化されるインスタンス ベースのスレッド ローカルな変数を作成できます。 次の例では、counter にアクセスするすべてのスレッドで開始値が 1 と見なされます。
Dim betterCounter As ThreadLocal(Of Integer) = New ThreadLocal(Of Integer)(Function() 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.
Dim threadLocalNumber As New ThreadLocal(Of Integer)(Function() Thread.CurrentThread.ManagedThreadId)
Dim t4 As New Thread(Sub()
Console.WriteLine("number on t4 = {0} threadID = {1}",
threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
End Sub)
t4.Start()
Dim t5 As New Thread(Sub()
Console.WriteLine("number on t5 = {0} threadID = {1}",
threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
End Sub)
t5.Start()
Dim t6 As New Thread(Sub()
Console.WriteLine("number on t6 = {0} threadID = {1}",
threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
End Sub)
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
// 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 オブジェクト全体を 1 つの Lazy<T> オブジェクトでラップするのではなく、それぞれの Order オブジェクトを必要になったときにのみ限定的に初期化したものとします。
' Assume that _orders contains null values, and
' we only need to initialize them if displayOrderInfo is true
If displayOrderInfo = True Then
For i As Integer = 0 To _orders.Length
' Lazily initialize the orders without wrapping them in a Lazy(Of T)
LazyInitializer.EnsureInitialized(_orders(i), Function()
' Returns the value that will be placed in the ref parameter.
Return GetOrderForIndex(i)
End Function)
Next
End If
// 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 のオーバーロードを使用します。
参照
処理手順
概念
その他の技術情報
履歴の変更
日付 |
履歴 |
理由 |
---|---|---|
2011 年 3 月 |
例外のキャッシュに関する情報を修正。 |
コンテンツ バグ修正 |
2011 年 4 月 |
例外のキャッシュに関する情報をさらに改訂。 |
コンテンツ バグ修正 |
2011 年 4 月 |
修正: Lazy<T>.ToString を呼び出しても、初期化は行われません。 |
コンテンツ バグ修正 |