Шаблон блокировки кэша с помощью C

#c #multithreading #locking

#c #многопоточность #блокировка

Вопрос:

Мне интересно, есть ли лучшая схема блокировки для кэша, чем простая блокировка:

 Mutex lock;
get(key) {
  LockGuard(lock);

  if (cache.has(key)) {
    return cache[key];
  } else {
    data = remoteclient.getslow();
    cache[key] = data;
    return data;
  }
}
  

Предполагая, что у вас много одинаковых запросов, вы каждый раз сериализуете доступ к get() . Можно ли сделать что-то более разумное с блокировками ReadWriter?

т.е. что, если вы сделаете что-то вроде:

 ReadersWritersLock lock;
get(key) {
  {
    ReadLockGuard(lock);
    if (cache.has(key)) {
      return cache[key];
    } 
  }
  WriteLockGuard(lock);
  data = remoteclient.getslow();
  cache[key] = data;
  return data;
 }
}
  

Теперь это позволит нескольким пользователям получать () одновременно в случае попадания в кэш. Однако, если два пользователя получат доступ к первому get() примерно в одно и то же время, возможно, что они оба попытаются перейти ко второй части кода, чтобы получить данные. Кажется ли это хорошей идеей?

Любые другие идеи по оптимизации такого рода кода?

Комментарии:

1. Есть ли у вас какой-либо фактический код в качестве эталонного примера? Помимо проблем с синтаксисом, таких как несоответствующие скобки и отсутствие возвращаемых типов, кажется странным, что метод get устанавливает кэш.

2. @AJG85: Совсем не странно; весь смысл «кэша» в том, что он кэширует результаты (медленного) запроса get.

3. @Nemo Это здорово, если данные не меняются, и в этом случае почему бы не предварительно кэшировать их при инициализации? Вместо отложенного инициализации вы можете захотеть разделить медленное извлечение и извлечение кэша, чтобы было проще реализовывать уведомления о том, когда кэш необходимо обновить и т.д.

4. Избегайте совместного использования памяти между потоками, так почему бы не поместить ваш кэш в локальное хранилище потока (или даже лучше в качестве параметра функции) и иметь по одному на поток?

5. @AJG85: я еще не написал код, поскольку пытаюсь решить, каким путем идти; Я не кэширую все, так как мне нужно только предварительно кэшировать подмножество данных, и этот набор изменяется со временем в зависимости от активности пользователя

Ответ №1:

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

 remoteclient.getslow();
  

вызывается, когда кэш заблокирован. Если remoteclient.getslow() на самом деле, вероятно, потребуется много времени для возврата (как указывает название), то любые другие потоки, пытающиеся получить доступ к кэшу, в конечном итоге будут заблокированы на долгое время (т. Е. До тех пор, пока getslow() не вернется, и поток, который его вызывал, снимет блокировку)… даже если их интересуют только несвязанные данные, которые уже присутствуют в кэше!

Чтобы избежать этого, я бы вызвал remoteclient.getslow() вместо этого вне области действия LockGuard (т. Е. Пока кэш разблокирован). Затем, после того, как remoteclient.getslow() вернет результат, я бы повторно заблокировал кеш и обновил кеш полученным значением. Таким образом, кэш никогда не блокируется на длительные периоды.

(Конечно, выполнение этого таким образом открывает возможность для нескольких потоков, вызывающих remoteclient.getslow() для одного и того же элемента данных, если все они решат, что им нужны одни и те же данные примерно в одно и то же время… но это может быть приемлемым побочным эффектом. Или, если нет, вы могли бы разработать механизм, указывающий, что определенное значение кэша находится в процессе извлечения, и блокировать другие потоки до завершения извлечения … если это стоит дополнительной сложности для вас. Для этого, вероятно, потребуются переменные условия и тому Подобное)

Ответ №2:

Ваш псевдокод имеет правильную идею, но у него есть условие гонки.

Как только ReadLockGuard выходит из области видимости, вы теряете блокировку, что означает, что структура данных может быть изменена другим потоком до того, как WriteLockGuard успеет перехватить блокировку.

Если ваша реализация блокировки чтения / записи поддерживает обновляемые блокировки, вам следует использовать это. В противном случае, после захвата блокировки для записи, вам необходимо дважды проверить кэш на случай, если он был заполнен между выпуском «reader» и получением «writer».

Комментарии:

1. Обновление — это именно то, что мне нужно! К сожалению, моя библиотека этого не поддерживает. Я знаю о состоянии гонки, о котором вы упомянули, но я думаю, что в моем случае все должно быть в порядке, поскольку это кэш, поэтому последующий вызов getslow() должен возвращать те же данные, ему просто нужно будет добавить больше нагрузки на другую сторону, но в итоге должен получить тот же результат.

2. Вы могли бы рассмотреть возможность использования повышающих мьютексов и блокировок, чтобы получить аналогичные возможности, а также обновляемые блокировки.

Ответ №3:

Возможно, что два потока войдут в «записывающую» часть get(), но, вероятно, очень маловероятно. Если вас беспокоит штраф за дополнительный вызов getslow(), вы можете снова проверить внутри блокировки записи.

 ReadersWritersLock lock;
get(key) {
  {
    ReadLockGuard(lock);
    if (cache.has(key)) {
      return cache[key];
    } 
  }
  WriteLockGuard(lock);
  if (cache.has(key) == false) {
    data = remoteclient.getslow();
    cache[key] = data;
    return data;
  }
 }
  

Комментарии:

1. Это заставляет меня задуматься, не сведет ли вся дополнительная работа по блокировке / разблокировке и поиску дважды на нет плюсы использования блокировки чтения-записи вообще. Я думаю, это зависит от того, насколько часты попадания в кэш..