Блокировка и обновление реализации

1

У меня есть ключ к сопоставлению задач, и мне нужно запустить задачу только в том случае, если задание для данного объекта еще не запущено. Далее следует псевдокод. Я считаю, что есть много возможностей для улучшения. Я блокирую карту и, следовательно, почти сериализую доступ к CacheFreshener. Есть ли лучший способ сделать это? Мы знаем, что когда я пытаюсь заблокировать ключ k1, нет смысла в вызове осведомителя кеша для ключа k2, ожидающего блокировки.

class CacheFreshener
{
     private ConcurrentDictionary<string,bool> lockMap;

     public RefreshData(string key, Func<string, bool> cacheMissAction)
     {
         lock(lockMap)
         {
             if (lockMap.ContainsKey(key))
             {
                 // no-op
                 return;
             }
             else
             {
                lockMap.Add(key, true); 
             }  
         }

         // if you are here means task is not already present
         cacheMissAction(key);

         lock(lockMap) // Do we need to lock here??
         {
             lockMap.Remove(key);
         }  
     }  

}
  • 1
    Вопросы типа Is there a better way of doing this? обычно идут в CodeReview SE.
  • 0
    @YoryeNathan Возможно, вы правы, однако псевдокод ВСЕГДА не по теме на Code Review .
Показать ещё 11 комментариев
Теги:
multithreading
caching
locking

1 ответ

0
Лучший ответ

По просьбе, вот подробное объяснение того, что я получаю по отношению к моим комментариям...

Основная проблема здесь, по-видимому, заключается в проблеме параллелизма, т.е. двух или более потоков, обращающихся к одному и тому же объекту за раз. Это сценарий ConcurrentDictionary предназначен для. Если вы используете методы IDictionary для ContainsKey() и Add() отдельно, вам понадобится явная синхронизация (но только для этой операции... в этом конкретном сценарии она не будет строго необходима при вызове Remove()), чтобы убедиться, что это выполненных как единая атомная операция. Но класс ConcurrentDictionary предвосхищает эту потребность и включает метод TryAdd() для достижения того же, без явной синхронизации.

< в сторону>
Мне не совсем ясно, в чем смысл примера кода. Кажется, что код предназначен только для хранения объекта в "кеше" в течение всего времени вызова делегата cacheMissAction. Ключ удаляется сразу после. Таким образом, похоже, что это не кэширование чего-либо как такового. Он просто препятствует тому, чтобы более одного потока cacheMissAction в процессе вызова cacheMissAction за один раз (последующие потоки не смогут его вызывать, но также не могут рассчитывать на то, что он завершил к моменту завершения их вызова метода RefreshData()).
</ в сторону>

Но, принимая приведенный пример кода, ясно, что явная блокировка не требуется. Класс ConcurrentDictionary уже обеспечивает потокобезопасный доступ (то есть не повреждение структуры данных при одновременном использовании из нескольких потоков), и он предоставляет метод TryAdd() как механизм для добавления ключа (и его значения, хотя здесь это просто всегда bool литерал true) в словарь, который будет гарантировать, что только один поток никогда не имеет ключ в словаре в то время.

Таким образом, мы можем переписать код, чтобы он выглядел так же и достигли той же цели:

private ConcurrentDictionary<string,bool> lockMap;

public RefreshData(string key, Func<string, bool> cacheMissAction)
{
    if (!lockMap.TryAdd(key, true))
    {
        return;
    }

    // if you are here means task was not already present
    cacheMissAction(key);

    lockMap.Remove(key);
}

TryAdd() lock не требуется для добавления или удаления, так как TryAdd() обрабатывает всю операцию "проверка на наличие ключа и добавление, если нет", атомарно.


Отмечу, что использование словаря для выполнения задания может считаться неэффективным. Если коллекция вряд ли будет большой, это не имеет большого значения, но мне кажется странным, что Microsoft решила сделать ту же самую ошибку, которую они изначально сделали, когда в дни до генерации вам пришлось использовать не общий Hashtable объект Hashtable для хранения набора, до появления HashSet<T>. Теперь у нас есть все эти простые в использовании классы в System.Collections.Concurrent, но там нет потокобезопасной реализации ISet<T>. Вздох…

Тем не менее, если вы предпочитаете несколько более эффективный подход с точки зрения хранения (это не обязательно более быстрая реализация, в зависимости от параллельных шаблонов доступа к объекту), что-то вроде этого будет работать как альтернатива:

private HashSet<string> lockSet;
private readonly object _lock = new object();

public RefreshData(string key, Func<string, bool> cacheMissAction)
{
    lock (_lock)
    {
        if (!lockSet.Add(key))
        {
            return;
        }
    }

    // if you are here means task was not already present
    cacheMissAction(key);

    lock (_lock)
    {
        lockSet.Remove(key);
    }
}

В этом случае вам нужен оператор lock, потому что класс HashSet<T> не является поточнобезопасным. Это, конечно, очень похоже на вашу первоначальную реализацию, просто используя более HashSet<T> семантику HashSet<T>.

  • 0
    Отличный ответ Питер! :)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню