Варианты оформления ссылок в поточно-безопасный кеш при удалении старых записей - PullRequest
3 голосов
/ 25 января 2010

Я пытаюсь создать простой кэш, который следует следующим правилам:

  1. Записи имеют уникальный ключ
  2. Когда количество записей в кеше превышает определенный лимит, старые элементы выселяются (чтобы кеш не становился слишком большим).
  3. Данные каждой записи неизменны, пока запись не будет удалена из кэша.
  4. «Считыватель» может получить доступ к записи в кэше, и запись должна быть действительна в течение всего времени жизни считывателя.
  5. Каждый читатель может быть в своем собственном потоке, и все читатели имеют доступ к одному и тому же экземпляру кэша.

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

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

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

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

Есть ли какие-либо другие шаблоны / идеи, которые я мог бы использовать для улучшения этого дизайна?

Ответы [ 6 ]

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

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

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

Если ваша структура является стандартным контейнером, который еще не является потокобезопасным, вам также придется защитить контейнер от неподдерживаемого одновременного доступа. Эта защита может прекрасно сочетаться с отсутствием условия гонки подсчета ссылок, описанного выше - если вы используете блокировку чтения-записи для защиты структуры в сочетании с атомарными приращениями счетчика ссылок в объекте, все еще удерживая блокировку чтения, вы будете защищен от любого, кто удаляет объект из-под вас, прежде чем вы получите счетчик ссылок, так как такие мутаторы должны быть «писателями».

Здесь объекты могут быть извлечены из кэша, при этом все еще имея положительный счетчик ссылок - они будут уничтожены при удалении последней невыполненной ссылки (вашим классом интеллектуальных указателей). Обычно это считается особенностью, поскольку это означает, что по крайней мере некоторый объект всегда может быть удален из кэша, но также имеет недостаток, заключающийся в том, что не существует строгого верхнего уровня для числа объектов, «живых» в памяти, поскольку подсчет ссылок позволяет объектам говорить живыми даже после того, как они покинули кеш. Приемлемо ли это для вас, зависит от ваших требований и деталей, например, как долго другие потоки могут хранить ссылки на объекты.

Если у вас нет доступа к (нестандартным) процедурам атомарного приращения, вы можете использовать мьютекс для выполнения атомарного приращения / уменьшения, хотя это может значительно увеличить стоимость как во времени, так и в пространстве на объект.

Если вы хотите стать более экзотичным (и более быстрым), вам нужно спроектировать контейнер, который сам по себе является потокобезопасным, и придумать более сложный механизм подсчета ссылок. Например, вы можете создать хеш-таблицу, в которой основной массив блоков никогда не перераспределяется, поэтому доступ к нему можно получить без блокировки. Кроме того, вы можете использовать непереносимые операции CAS (сравнить и поменять) двойной ширины в этом массиве, чтобы одновременно прочитать указатель и увеличить счетчик ссылок рядом с ним (128 бит данных в 64-битной арке), что позволяет вам избегайте гонки, упомянутой выше.

Совершенно другой путь - реализация какой-то стратегии "отложенного безопасного удаления". Здесь избегайте подсчета ссылок полностью. Вы удаляете ссылки из своего кэша, но не удаляете объекты немедленно, так как другие потоки могут все еще содержать указатели на объект. Затем, в какое-то «безопасное» время, вы удаляете объект. Конечно, дело в том, чтобы обнаружить, когда такое безопасное время существует. Базовые стратегии предусматривают сигнализацию каждого потока, когда они «входят» и «покидают» опасную зону , во время которой они могут получить доступ к кешу и хранить ссылки на содержащиеся в нем объекты. После того, как все потоки, которые находились в опасной зоне , когда объект был удален из кэша, покинули опасную зону, вы можете освободить объект, убедившись, что ссылки больше не удерживаются.

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

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

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

Я думаю, что вы действительно хотите блокировку чтения / записи для каждой записи. Читатели читают блокировку и разблокировку, когда они ее используют. Поток выселения должен получить блокировку записи (которая заставляет всех читателей завершить работу, прежде чем она может быть получена) Для читателя должен быть какой-то способ затем сказать (до получения блокировки чтения), была ли данная запись одновременно выселена.

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

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

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

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

Мне кажется, что решение для подсчета ссылок является лишь хитрым в том смысле, что обновление / тестирование для удаления указанных счетчиков ссылок должно быть внутри критической секции, защищенной мьютексом. Поскольку более чем один процесс не имеет доступа к счетчикам ссылок одновременно, он должен быть потокобезопасным.

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

Я думаю, что если вы хотите поделиться ссылкой, вам нужно вести подсчет. Пока вы используете взаимосвязанные функции inc / dec, это должно быть достаточно просто даже для нескольких потоков.

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

Звучит как монитор с std :: map в качестве буфера, который будет полезен в этой ситуации.

...