ConcurrentDictionary と Lazy を使用して同時に要素を追加した時の初期化を1回だけにする方法

2024年9月18日水曜日

C#

t f B! P L

概要

この記事では、.NET(C#)のConcurrentDictionaryLazy を組み合わせて、マルチスレッド環境下で要素を安全に行う方法を解説します。

スレッドセーフな辞書構造を必要とするシナリオでは、ConcurrentDictionary を一般的に選択します。(Dictionaryは変更操作がスレッドセーフでないため)
ただし、ConcurrentDictionaryは要素の単一性は保証しますが、複数スレッドから要素を追加する際の同期制御はしないため、同時に要素を追加すると、同じ要素に対する初期化が複数走ってしまう可能性があります。
初期化を1回だけに制御をする場合は、これにLazy を組み合わせることで、要素が一度だけ初期化されるようにできます。

まずは、ConcurrentDictionaryLazyの概要に触れていきましょう。

ConcurrentDictionary の概要

ConcurrentDictionary は、.NET で提供されるスレッドセーフなディクショナリです。標準の Dictionary と異なり、複数のスレッドからの同時アクセスに対してロックやブロッキングなしで安全に動作します。
ConcurrentDictionary では、以下のような機能が提供されます:

  1. 複数スレッドからの安全な読み書き
  2. 要素の追加や更新を原子操作で実行
  3. スレッド間での競合を最小限に抑える内部のロック分割

Lazy の概要

Lazy は、オブジェクトの初期化を遅延させるための仕組みを提供するクラスです。通常、オブジェクトが実際に必要になるまで初期化されません。これにより、必要な時にだけコストをかけて初期化することができ、特に重い初期化処理が絡む場合に役立ちます。また、Lazy のインスタンスは、スレッドセーフな初期化もサポートしているため、マルチスレッド環境で使う際にも便利です。

ConcurrentDictionary と Lazy を組み合わせることで…

ConcurrentDictionaryLazyの特徴を組み合わせることで、「スレッドセーフで要素の追加/削除が可能」「要素の初期化が一度しか行われない」ディクショナリが実現可能になります。また、Lazy により、必要になるまでオブジェクトの初期化が行われないため、無駄なリソース消費を防ぐことができます。

ConcurrentDictionary と Lazy を使った実装例

以下のコードでは、ConcurrentDictionaryLazy を組み合わせて、要素の追加が遅延初期化され、マルチスレッド環境でも、要素が一度だけ初期化されるディクショナリを実装します。

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

実装のポイント

上で紹介したサンプルコードの解説を少しします。

  1. ConcurrentDictionary の使用: _dictionaryConcurrentDictionary<int, Lazy<string>> 型で、int がキー、Lazy<string> が値になります。これにより、値の初期化が遅延されます。

  2. GetOrAdd メソッド: GetOrAdd メソッドを使うことで、指定したキーが存在しない場合にのみ値を追加し、既に存在する場合はその値を取得できます。ここでは、Lazy オブジェクトを利用して、複数のスレッドが同時にアクセスした場合でも一度しか初期化が行われないことを保証します。

  3. Lazy.Value の取得: 実際に値を使用する際には、Lazy<string>Value プロパティを使用して、遅延初期化された値を取得します。このプロパティがアクセスされた時点で初めて、値の初期化が行われます。

  4. 初期化処理の重複防止: 複数のスレッドが同時に AddOrGetValue を呼び出しても、Lazy によって初期化処理が一度だけ行われるため、無駄な重複処理を防げます。

まとめ

ConcurrentDictionaryLazy を組み合わせることで、マルチスレッド環境においてスレッドセーフなディクショナリを簡単に作成することができます。これにより、複数のスレッドが同時に同じキーにアクセスしても、安全かつ効率的に値を初期化し管理できます。

スポンサーリンク
スポンサーリンク

このブログを検索

Profile

自分の写真
Webアプリエンジニア。 日々新しい技術を追い求めてブログでアウトプットしています。
プロフィール画像は、猫村ゆゆこ様に書いてもらいました。

仕事募集もしていたり、していなかったり。

QooQ