0

私は 3.5 .NET Framework を使用して開発しており、アイテムの遅延読み込みパターンを使用してマルチスレッド シナリオでキャッシュを使用する必要があります。Web でいくつかの記事を読んだ後、独自の実装を作成しようとしました。

public class CacheItem
{
    public void ExpensiveLoad()
    {
        // some expensive code
    }
}
public class Cache
{
    static object SynchObj = new object();
    static Dictionary<string, CacheItem> Cache = new Dictionary<string, CacheItem>();
    static volatile List<string> CacheKeys = new List<string>();

    public CacheItem Get(string key)
    {
        List<string> keys = CacheKeys;
        if (!keys.Contains(key))
        {
            lock (SynchObj)
            {
                keys = CacheKeys;
                if (!keys.Contains(key))
                {
                    CacheItem item = new CacheItem();
                    item.ExpensiveLoad();
                    Cache.Add(key, item);
                    List<string> newKeys = new List<string>(CacheKeys);
                    newKeys.Add(key);
                    CacheKeys = newKeys;
                }
            }
        }
        return Cache[key];
    }
}

ご覧のとおり、Cache オブジェクトは、実際のキーと値のペアを格納するディクショナリと、キーのみを複製するリストの両方を使用します。スレッドが Get メソッドを呼び出すと、静的な共有キー リスト (volatile と宣言されている) を読み取り、Contains メソッドを呼び出して、キーが既に存在するかどうかを確認し、存在しない場合は、遅延読み込みを開始する前に二重チェックのロック パターンを使用します。読み込みの最後に、キー リストの新しいインスタンスが作成され、静的変数に格納されます。

明らかに、キーのリスト全体を再作成するコストは、単一のアイテムをロードするコストとはほとんど無関係です。

それが本当にスレッドセーフかどうか、誰かが教えてくれることを願っています。「スレッドセーフ」とは、すべてのリーダー スレッドが破損またはダーティ リードを回避でき、すべてのライター スレッドが不足しているアイテムを 1 回だけロードすることを意味します。

4

2 に答える 2

7

辞書を読むときにロックしていないため、これはスレッドセーフではありません。

1 つのスレッドが読み取ることができる競合状態があります。

return Cache[key];

別の人が書いている間:

_Cache.Add(key, item);

州のMSDNドキュメントとしてDictionary<TKey,TValue>: `

読み取りおよび書き込みのために複数のスレッドがコレクションにアクセスできるようにするには、独自の同期を実装する必要があります。

同期にはリーダーが含まれていません。

コードを大幅に簡素化するスレッドセーフな辞書を使用する必要があります (リストはまったく必要ありません)。

.NET 4 ConcurrentDictionary のソースを取得することをお勧めします。

他の回答者の一部が実装がスレッドセーフであると誤って述べているという事実によって証明されるように、スレッドセーフを正しくすることは困難です。したがって、自家製の実装よりも前に、Microsoft の実装を信頼します。

スレッドセーフな辞書を使用したくない場合は、次のような簡単なものをお勧めします。

public CacheItem Get(string key)
{
    lock (SynchObj)
    {
        CacheItem item;
        if (!Cache.TryGetValue(key, out item))
        {
            item = new CacheItem();
            item.ExpensiveLoad();
            Cache.Add(key, item);
        }
        return item;
    }
}

を使用して実装を試すこともできReaderWriterLockSlimますが、パフォーマンスが大幅に向上しない場合があります (ReaderWriterLockSlim のパフォーマンスについては Google を参照)。

ConcurrentDictionary を使用した実装に関しては、ほとんどの場合、単純に次のようなものを使用します。

static ConcurrentDictionary<string, CacheItem> Cache = 
    new ConcurrentDictionary<string, CacheItem>(StringComparer.Ordinal);
...
CacheItem item = Cache.GetOrAdd(key, key => ExpensiveLoad(key));

これにより、ExpensiveLoadキーごとに複数回呼び出される可能性がありますが、アプリのプロファイルを作成すると、これが非常にまれで問題にならないことがわかるでしょう。

一度だけ呼び出されることを本当に主張する場合は、.NET 4Lazy<T>実装を手に入れて、次のようなことを行うことができます。

static ConcurrentDictionary<string, Lazy<CacheItem>> Cache = 
    new ConcurrentDictionary<string, Lazy<CacheItem>>(StringComparer.Ordinal);
...

CacheItem item = Cache.GetOrAdd(key, 
               new Lazy<CacheItem>(()=> ExpensiveLoad(key))
             ).Value;

このバージョンでは、複数のLazy<CacheItem>インスタンスが作成される可能性がありますが、実際にディクショナリに格納されるのは 1 つだけです。 ディクショナリに格納されたインスタンスに対してExpensiveLoad初めて逆参照されるときに呼び出されます。Lazy<CacheItem>.ValueこのLazy<T>コンストラクターは、内部でロックを使用する LazyThreadSafetyMode.ExecutionAndPublication を使用するため、1 つのスレッドのみがファクトリ メソッドを呼び出すようにしますExpensiveLoad

余談ですが、文字列キーを使用してディクショナリを作成するときは、常にIEqualityComparer<string>パラメーター (通常は StringComparer.Ordinal または StringComparer.OrdinalIgnoreCase) を使用して、大文字と小文字の区別に関する意図を明示的に文書化します。

于 2012-09-13T14:26:09.683 に答える
0

今のところ大きな問題は見当たりません。あなたのコードで私が見ることができない唯一のものは、どのようにCacheKeys公開するのですか? 最も単純なものはIList<string>、 a で満たされているasReadOnlyCollectionです。このようにして、コンシューマーはインデックス演算子または count プロパティを非常に簡単に使用できます。この場合、volatileすでにすべてをロックに入れているため、キーワードも必要ありません。だから私はあなたのクラスを次のようにポン引きします:

public class CacheItem
{
    public void ExpensiveLoad()
    {
        // some expensive code
    }
}
public class Cache
{
    private static object _SynchObj = new object();
    private static Dictionary<string, CacheItem> _Cache = new Dictionary<string, CacheItem>();
    private static ReadOnlyCollection<string> _CacheKeysReadOnly = new ReadOnlyCollection(new List<string>());

    public IList<string> CacheKeys
    {
        get
        {
            return _CacheKeysReadOnly;
        }
    }

    public CacheItem Get(string key)
    {
        CacheItem item = null;
        ReadOnlyCollection<string> keys = _CacheKeysReadOnly;
        if (!keys.Contains(key))
        {
            lock (_SynchObj)
            {
                keys = _CacheKeysReadOnly;
                if (!keys.Contains(key))
                {
                    item = new CacheItem();
                    item.ExpensiveLoad();
                    _Cache.Add(key, item);
                    List<string> newKeys = new List<string>(_CacheKeysReadOnly);
                    newKeys.Add(key);
                    _CacheKeysReadOnly = newKeys.AsReadOnly();
                }
            }
        }
        return item;
    }
}

すでに .Net 4.5 を使用している場合の代わりIReadOnlyList<T>に、プロパティのインターフェイスを使用することも考えられCacheKeysます。

于 2012-09-13T10:09:56.897 に答える