Как я могу сказать `ConcurrentDictionary.GetOrAdd` не добавлять значение? - PullRequest
10 голосов
/ 28 марта 2012

У меня есть несколько случаев, когда я использую ConcurrentDictionary<TKey, TValue> для кэширования значений, но часто мне нужно выполнить проверку значения, чтобы решить, добавлять ли его в кэш, используя ConcurrentDictionary<TKey, TValue>.GetOrAdd(TKey, Func<TKey, TValue>).

Обычнов соответствии с:

private readonly ConcurrentDictionary<Type, ISomeObject> someObjectCache = 
    new ConcurrentDictionary<Type, ISomeObject>();
public ISomeObject CreateSomeObject(Type someType)
{
    return someObjectCache.GetOrAdd(someType, type =>
    {
        if(!Attribute.IsDefined(someType, typeof(SomeAttribute))
            // Do something here to avoid the instance from being added to
            //    `someObjectCache`

        ISomeObject someObject;
        // Typical factory functionality goes here
        return someObject;
    });
}

Способ, которым я справляюсь сегодня, состоит в том, чтобы создать исключение, которое, кажется, работает нормально, но я бы хотел более чистый подход (возможно, флаг, который я могу установить, или конкретное значениеЯ могу установить возвращаемое значение в), чтобы отменить GetOrAdd из лямбды (хотя это может быть реально заменено полным методом).

Основываясь на моем опыте с другими подобными LINQ методами, возвращая null приведет к тому, что значение будет добавлено без проверки как таковой (и чтение IL для GetOrAdd похоже, что это приведет к той же проблеме), поэтому я не думаю, что это сработает.

Есть ли способ избежать исключений для отмены добавления с помощью GetOrAdd?

Ответы [ 2 ]

10 голосов
/ 28 марта 2012

Из того, что я прочитал, нет гарантии, что метод фабрики Add будет вызываться только один раз среди всех вызывающих, чтобы получить один и тот же ключ .

Соответствующая часть этой страницы находится внизу, здесь указано:

Кроме того, хотя все методы ConcurrentDictionary (Of TKey, TValue) потокобезопасны, не все методы являются атомарными, в частности GetOrAdd и AddOrUpdate. Пользовательский делегат, который передается этим методам: вызывается вне внутренней блокировки словаря. (Это сделано для предотвратить неизвестный код от блокировки всех потоков.) Поэтому это возможно для этой последовательности событий произойти:

1) threadA вызывает GetOrAdd, не находит элемента и создает новый элемент для добавления. вызывая делегат valueFactory.

2) threadB одновременно вызывает GetOrAdd, его делегат valueFactory вызывается, и он достигает внутренней блокировки перед threadA, и поэтому его новая пара ключ-значение добавлена ​​в словарь.

3) Пользовательский делегат threadA завершается, и поток достигает заблокировать, но теперь видит, что элемент уже существует

4) threadA выполняет «Get» и возвращает данные, которые были ранее добавлено автором B.

Следовательно, не гарантируется, что данные, возвращаемые GetOrAdd - это те же данные, которые были созданы потоком valueFactory. Подобная последовательность событий может произойти, когда AddOrUpdate называется.

Способ, который я прочитал, заключается в том, что даже если вы вызываете какую-то блокировку в вашем делегате Add, вы не гарантируете, что значение, возвращаемое из вашего add, будет тем, которое будет фактически использоваться.

Итак, вам не нужно добавлять какие-либо дополнительные блокировки, и вместо этого вы можете использовать следующий шаблон:

private ConcurrentDictionary<Type, ISomeObject> someObjectCache = 
    new ConcurrentDictionary<Type, ISomeObject>();
public ISomeObject CreateSomeObject(Type someType)
{

    ISomeObject someObject; 
    if (someObjectCache.TryGet(someType, out someObject))
    {
       return someObject;
    }

    if (Attribute.IsDefined(someType, typeof(SomeAttribute)) 
    { 
        // init someObject here
        someObject = new SomeObject(); 

        return someObjectCache.GetOrAdd(someType, someObject); // If another thread got through here first, we'll return their object here. 
    }

    // fallback functionality goes here if it doesn't have your attribute. 
}

Да, это может привести к потенциальной возможности создания новых объектов несколько раз, но все вызывающие стороны получат один и тот же результат, даже если вызывается несколько. То же, что и GetOrAdd сейчас.

3 голосов
/ 08 апреля 2012

Все проблемы в информатике могут быть решены с помощью другого уровня косвенности

// the dictionary now stores functions
private readonly ConcurrentDictionary<Type, Func<ISomeObject>> someObjectCache =
  new ConcurrentDictionary<Type, Func<ISomeObject>>();

public ISomeObject CreateSomeObject(Type someType) {
  return someObjectCache.GetOrAdd(someType, _ => {
    if(ShouldCache(someType)) {
      // caching should be used
      // return a function that returns a cached instance
      var someObject = Create(someType);
      return () => someObject; 
    }
    else {
      // no caching should be used
      // return a function that always creates a new instance
      return () => Create(someType); 
    }
  })(); // call the returned function
}

private bool ShouldCache(Type someType) {
  return Attribute.IsDefined(someType, typeof(SomeAttribute));
}

private ISomeObject Create(Type someType) {
  // typical factory functionality ...
}

Теперь значение, хранящееся в словаре, является функцией ; когда вы не хотите, чтобы происходило кэширование, функция всегда создает новый экземпляр; когда вы хотите, чтобы происходило кэширование, функция возвращает кэшированный экземпляр.

...