概要
この記事では、.NET(C#)のConcurrentDictionary
と Lazy
を組み合わせて、マルチスレッド環境下で要素を安全に行う方法を解説します。
スレッドセーフな辞書構造を必要とするシナリオでは、ConcurrentDictionary
を一般的に選択します。(Dictionary
は変更操作がスレッドセーフでないため)
ただし、ConcurrentDictionary
は要素の単一性は保証しますが、複数スレッドから要素を追加する際の同期制御はしないため、同時に要素を追加すると、同じ要素に対する初期化が複数走ってしまう可能性があります。
初期化を1回だけに制御をする場合は、これにLazy
を組み合わせることで、要素が一度だけ初期化されるようにできます。
まずは、ConcurrentDictionary
とLazy
の概要に触れていきましょう。
ConcurrentDictionary の概要
ConcurrentDictionary
は、.NET で提供されるスレッドセーフなディクショナリです。標準の Dictionary
と異なり、複数のスレッドからの同時アクセスに対してロックやブロッキングなしで安全に動作します。
ConcurrentDictionary
では、以下のような機能が提供されます:
- 複数スレッドからの安全な読み書き
- 要素の追加や更新を原子操作で実行
- スレッド間での競合を最小限に抑える内部のロック分割
Lazy
の概要
Lazy
は、オブジェクトの初期化を遅延させるための仕組みを提供するクラスです。通常、オブジェクトが実際に必要になるまで初期化されません。これにより、必要な時にだけコストをかけて初期化することができ、特に重い初期化処理が絡む場合に役立ちます。また、Lazy
のインスタンスは、スレッドセーフな初期化もサポートしているため、マルチスレッド環境で使う際にも便利です。
ConcurrentDictionary と Lazy を組み合わせることで…
ConcurrentDictionary
とLazy
の特徴を組み合わせることで、「スレッドセーフで要素の追加/削除が可能」「要素の初期化が一度しか行われない」ディクショナリが実現可能になります。また、Lazy
により、必要になるまでオブジェクトの初期化が行われないため、無駄なリソース消費を防ぐことができます。
ConcurrentDictionary と Lazy を使った実装例
以下のコードでは、ConcurrentDictionary
と Lazy
を組み合わせて、要素の追加が遅延初期化され、マルチスレッド環境でも、要素が一度だけ初期化されるディクショナリを実装します。
using System;
using System.Collections.Concurrent;
public class Program
{
// ConcurrentDictionaryでキーはint型、値はLazy<string>型
private static ConcurrentDictionary<int, Lazy<string>> _dictionary = new ConcurrentDictionary<int, Lazy<string>>();
public static void Main()
{
// スレッドプールを使って並列に辞書にアクセス
Parallel.Invoke(
() => AddOrGetValue(1, () => ExpensiveInitialization("Value for 1")),
() => AddOrGetValue(2, () => ExpensiveInitialization("Value for 2")),
() => AddOrGetValue(1, () => ExpensiveInitialization("Value for 1")),
() => AddOrGetValue(3, () => ExpensiveInitialization("Value for 3"))
);
// 辞書の内容を表示
foreach (var kvp in _dictionary)
{
Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value.Value}");
}
}
// 値を取得、または辞書に追加するメソッド
private static string AddOrGetValue(int key, Func<string> valueFactory)
{
// Lazyオブジェクトを使って値を遅延初期化
var lazyValue = _dictionary.GetOrAdd(key, k => new Lazy<string>(valueFactory));
// 初期化された値を返す
return lazyValue.Value;
}
// 高コストの初期化処理をシミュレートするメソッド
private static string ExpensiveInitialization(string value)
{
Console.WriteLine($"Initializing: {value}");
return value;
}
}
実装のポイント
上で紹介したサンプルコードの解説を少しします。
-
ConcurrentDictionary
の使用:_dictionary
はConcurrentDictionary<int, Lazy<string>>
型で、int
がキー、Lazy<string>
が値になります。これにより、値の初期化が遅延されます。 -
GetOrAdd
メソッド:GetOrAdd
メソッドを使うことで、指定したキーが存在しない場合にのみ値を追加し、既に存在する場合はその値を取得できます。ここでは、Lazy
オブジェクトを利用して、複数のスレッドが同時にアクセスした場合でも一度しか初期化が行われないことを保証します。 -
Lazy.Value
の取得: 実際に値を使用する際には、Lazy<string>
のValue
プロパティを使用して、遅延初期化された値を取得します。このプロパティがアクセスされた時点で初めて、値の初期化が行われます。 -
初期化処理の重複防止: 複数のスレッドが同時に
AddOrGetValue
を呼び出しても、Lazy
によって初期化処理が一度だけ行われるため、無駄な重複処理を防げます。
まとめ
ConcurrentDictionary
と Lazy
を組み合わせることで、マルチスレッド環境においてスレッドセーフなディクショナリを簡単に作成することができます。これにより、複数のスレッドが同時に同じキーにアクセスしても、安全かつ効率的に値を初期化し管理できます。
0 件のコメント:
コメントを投稿