Варианты дизайна для поточного кеша C ++ - PullRequest
4 голосов
/ 26 января 2010

Я нахожусь в процессе написания библиотеки шаблонов для кеширования данных на C ++, где можно выполнять одновременное чтение и одновременную запись, но не для одного и того же ключа. Шаблон можно объяснить следующей средой:

  1. Мьютекс для записи в кеш.
  2. Мьютекс для каждого ключа в кэше.

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

Основные ограничения:

  1. Никогда не вычисляйте значение для ключа одновременно.
  2. Вычисление значения для 2 различных клавиш может выполняться одновременно.
  3. Извлечение данных не должно блокировать другие потоки для извлечения данных из других ключей.

Мои другие ограничения, но уже разрешенные:

  1. исправлен (известен во время компиляции) максимальный размер кеша с использованием MRU (наиболее недавно использовавшегося) перебора.
  2. поиск по ссылке (косвенный подсчет с мьютексированием)

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

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

Ответы [ 3 ]

3 голосов
/ 26 января 2010

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

2 голосов
/ 26 января 2010

Вы хотите заблокировать и хотите подождать.Таким образом, где-то должны быть «условия» (как pthread_cond_t в Unix-подобных системах).

Я предлагаю следующее:

  • Существует глобальный мьютекс, который используется только длядобавить или удалить ключи на карте.
  • Карта отображает ключи на значения, значения которых являются обертками.Каждая оболочка содержит условие и, возможно, значение.Условие сигнализируется, когда значение установлено.

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

  1. Если для этого ключа есть оболочка, и эта оболочка содержит значение, то поток имеет свое значение и может освободить глобальный мьютекс.
  2. Если для этого ключа есть обертка, но пока нет значения, это означает, что какой-то другой поток в данный момент занят вычислением значения.Затем поток блокирует условие, чтобы оно было вызвано другим потоком после его завершения.
  3. Если обертки нет, то поток регистрирует новую обертку на карте, а затем переходит к вычислению значения,Когда значение вычисляется, оно устанавливает значение и сигнализирует о состоянии.

В псевдокоде это выглядит следующим образом:

mutex_t global_mutex
hashmap_t map

lock(global_mutex)
w = map.get(key)
if (w == NULL) {
    w = new Wrapper
    map.put(key, w)
    unlock(global_mutex)
    v = compute_value()
    lock(global_mutex)
    w.set(v)
    signal(w.cond)
    unlock(global_mutex)
    return v
} else {
    v = w.get()
    while (v == NULL) {
        unlock-and-wait(global_mutex, w.cond)
        v = w.get()
    }
    unlock(global_mutex)
    return v
}

В pthreads терминах, lockравно pthread_mutex_lock(), unlock равно pthread_mutex_unlock(), unlock-and-wait равно pthread_cond_wait() и signal равно pthread_cond_signal().unlock-and-wait атомарно освобождает мьютекс и помечает поток как ожидающий при условии;когда нить пробуждается, мьютекс автоматически восстанавливается.

Это означает, что каждая оболочка должна содержать условие.Это воплощает ваши различные требования:

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

Обратите внимание, что когда поток хочет получить значение и обнаруживает, что какой-то другой поток уже существуетЗанимаясь его вычислением, потоки блокируют глобальный мьютекс дважды: один раз в начале и один раз, когда значение становится доступным.Более сложное решение, с одним мьютексом на обертку, может избежать второй блокировки, но если конкуренция не очень высока, я сомневаюсь, что это стоит усилий.

Насчет множества мьютексов: мьютексы дешевы.Мьютекс - это, по сути, int, он стоит всего четыре байта оперативной памяти, используемой для его хранения.Остерегайтесь терминологии Windows: в Win32 то, что я здесь называю мьютексом, считается «взаимосвязанной областью»;то, что создает Win32 при вызове CreateMutex(), является чем-то совершенно другим, доступным из нескольких отдельных процессов и намного более дорогостоящим, поскольку включает в себя обращения к ядру.Обратите внимание, что в Java каждый экземпляр объекта содержит мьютекс, и разработчики Java, похоже, не слишком раздражительны в этом вопросе.

0 голосов
/ 26 января 2010

Одной из возможностей, которая была бы намного более простым решением, было бы использование одной блокировки чтения / записи на весь кэш. Учитывая, что вы знаете, что существует максимальное количество записей (и оно относительно небольшое), похоже, что добавление новых ключей в кеш - это "редкое" событие. Общая логика будет:

acquire read lock
search for key
if found
    use the key
else
    release read lock
    acquire write lock
    add key
    release write lock
    // acquire the read lock again and use it (probably encapsulate in a method)
endif

Не зная больше о шаблонах использования, я не могу точно сказать, является ли это хорошим решением. Тем не менее, это очень просто, и если использование в основном читается, то это очень недорого с точки зрения блокировки.

...