web-dev-qa-db-ja.com

マルチスレッドシナリオでDictionaryオブジェクトのset_itemメソッドを呼び出すときにNullReferenceExceptionをスローします

当社のウェブサイトには、「config.aspx」などの構成ページがあり、ページの初期化時に構成ファイルからいくつかの情報が読み込まれます。ロードされた情報をキャッシュするために、ファクトリクラスを提供し、ファクトリのパブリックメソッドを呼び出して、ページがロードされたときに構成インスタンスを取得します。ただし、アプリケーションプールを再起動すると、イベントログに次のようなエラーメッセージが表示されることがあります。

メッセージ:オブジェクト参照がオブジェクトのインスタンスに設定されていません。
スタック:System.Collections.Generic.Dictionary`2.Insert(TKey key、TValue value、Boolean add)
 at System.Collections.Generic.Dictionary`2.set_Item(TKey key、TValue value)
 at ObjectFactory.GetInstance(string key)
 at config.Page_Load(Object sender、EventArgs e) 
 at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp、Object o、Object t、EventArgs e)
 at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender、EventArgs e) 
 at System.Web.UI.Control.OnLoad(EventArgs e)
 at System.Web.UI.Control.LoadRecursive()
 atSystem.Web.UI.Page。 ProcessRequestMain(ブール値includeStagesBeforeAsyncPoint、ブール値includeStagesAfterAsyncPoint)

ファクトリクラスは次のように実装します。


public static class ObjectFactory
{
    private static object _InternalSyncObject;
    private static Dictionary _Instances;

    private static object InternalSyncObject
    {
        get
        {
            if (_InternalSyncObject == null)
            {
                var @object = new object();
                Interlocked.CompareExchange(ref _InternalSyncObject, @object, null);
            }

            return _InternalSyncObject;
        }
    }

    private static Dictionary Instances
    {
        get
        {
            if (_Instances == null)
            {
                lock (InternalSyncObject)
                {
                    if (_Instances == null)
                    {
                        _Instances = new Dictionary();
                    }
                }
            }

            return _Instances;
        }
    }

    private static object LoadInstance(string key)
    {
        object obj = null;

        // some statements to load an specific instance from a configuration file.

        return obj;
    }

    public static object GetInstance(string key)
    {
        object instance;

        if (false == Instances.TryGetValue(key, out instance))
        {
            instance = LoadInstance(key);

            Instances[key] = instance;
        }

        return instance;
    }
} 

辞書のset_Itemメソッドを呼び出すことができる唯一のコードであるため、例外は「Instances [key] = instance;」という行によってスローされたと思います。ただし、「Instances」値がnullの場合、NullReferenceExceptionメソッドを呼び出すとTryGetValueがスローされ、スタックトレースのトップフレームはGetInstanceではなくInsertである必要があります。マルチスレッドシナリオでset_Itemメソッドを呼び出すときに、辞書がNullReferenceExceptionをスローする方法を知っている人はいますか?

42
Pag Sun

例外はDictionaryコードの内部で発生するため、複数のスレッドから同時に同じDictionaryインスタンスにアクセスしていることを意味します。

GetInstanceメソッドのコードを同期して、一度に1つのスレッドだけがDictionaryにアクセスするようにする必要があります。

編集:
アクセスを個別にロックして、(おそらく)時間のかかるロードを実行しているときにロックの内側に入らないようにします。

private static object _sync = new object();

public static object GetInstance(string key) {
   object instance = null;
   bool found;
   lock (_sync) {
      found = Instances.TryGetValue(key, out instance);
   }
   if (!found) {
      instance = LoadInstance(key);
      lock (_sync) {
         object current;
         if (Instances.TryGetValue(key, out current)) {
            // some other thread already loaded the object, so we use that instead
            instance = current;
         } else {
            Instances[key] = instance;
         }
      }
   }
   return instance;
}
51
Guffa

.Net 4の時点で、スレッドセーフな辞書である ConcurrentDictionary があり、「手動」同期は不要です。

33
jaraics

引用するには http://msdn.Microsoft.com/en-us/library/xfhwa508.aspx (私が強調を追加):

「」スレッドセーフ

このタイプのパブリック静的(Visual Basicで共有)メンバーはスレッドセーフです。インスタンスメンバーは、スレッドセーフであることが保証されていません。

Dictionary<(Of <(TKey, TValue>)>)は、コレクションが変更されていない限り、複数のリーダーを同時にサポートできます。それでも、コレクションを介して列挙することは、本質的にスレッドセーフな手順ではありません。列挙型が書き込みアクセスと競合するまれなケースでは、列挙型全体でコレクションをロックする必要があります。 読み取りと書き込みのために複数のスレッドがコレクションにアクセスできるようにするには、独自の同期を実装する必要があります。」

5
Ian Kemp

より良い解決策は、同期された辞書を作成することです。これがこの状況で機能するものです。 ReaderWriterLockSlimは、この状況で使用するのに最適な同期オブジェクトだと思います。辞書への書き込みは非常にまれです。ほとんどの場合、キーは辞書にあります。私は辞書のすべてのメソッドを実装したわけではなく、この場合に使用されたメソッドだけを実装したため、完全に同期された辞書ではありません。

public sealed class SynchronizedDictionary<TKey, TValue>
{
    private readonly Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue>();
    private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();

    public TValue this[TKey key]
    {
        get
        {
            readerWriterLock.EnterReadLock();
            try
            {
                return this.dictionary[key];
            }
            finally
            {
                readerWriterLock.ExitReadLock();
            }
        }
        set
        {
            readerWriterLock.EnterWriteLock();
            try
            {
                this.dictionary[key] = value;
            }
            finally
            {
                readerWriterLock.ExitWriteLock();
            }
        }
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        readerWriterLock.EnterReadLock();
        try
        {
            return this.dictionary.TryGetValue(key, out value);
        }
        finally
        {
            readerWriterLock.ExitReadLock();
        }
    }
}
1
Bryan

あなたのInstancesDictionaryはnullではないと思います。例外はInsertメソッドの内部からです-実行時にDictionaryオブジェクトがあることを意味します(また、あなたが言ったように、あなたはすでにTryGetValueを以前に持っていました同じ参照)keyがnullである可能性がありますか?

編集

チェックしたばかりです-TryGetValueは、nullキーを受信するとArgumentNullExceptionをスローし、nullキーを使用して挿入します。しかし、あなたの例ではどのクラスを使用していますか?ジェネリックIDictionary<string, string>を使用しましたが、非ジェネリックを使用しているようです。 DictionaryBaseから継承したクラスですか、それともHashTableから継承したクラスですか?

1
Noam Gal