Предположим, у нас есть карта, которая используется несколькими потоками. Он представляет собой узел в некоторой иерархической структуре (скажем, каталог в файловой системе), который хранится на диске. Построение значения дорого как во времени, так и в памяти. Это классическая проблема «инициализации по требованию», с изюминкой: мы инициализируем значения на карте, когда есть запрос поиска, но мы не хотим блокировать карту whole , пока мы делаем это для того, чтобы другие потоки имели доступ к уже созданным значениям. В этом приложении поиск существующих значений будет намного более распространенным, чем несуществующие.
Попытка 1 : захватить блокировку записи на карте, проверить наличие ключа, вернуть, если имеется, в противном случае построить значение, вставить в карту.
Оценка : Это предотвращает доступ других потоков к карте во время создания значения. Поскольку чтения очень распространены и очень быстрые, это проявится как ужасный скачок задержки: не хорошо.
Попытка 2 : захватить блокировку чтения на карте, проверить наличие ключа, вернуть, если имеется, в противном случае снять блокировку чтения, создать значение, захватить блокировку записи, проверить наличие, если нет, вставить карту, если она есть, удалить вновь созданное значение.
Оценка : Теперь мы не несем скачок в задержке чтения, но мы можем закончить ненужным построением нескольких представлений в памяти одного и того же значения (когда несколько потоков пытаются найти один и тот же Неограниченное значение одновременно). Проблема в том, что мы не хотим этого: эти значения действительно дороги в создании. Кроме того, они могут начать запускать события или выполнять операции ввода-вывода, что означает, что теперь нам приходится иметь дело с проектом, который позволяет существовать эфемерным, но в то же время тяжелым экземплярам Value: дополнительной головной боли лучше избегать.
Попытка 3 : использовать двухуровневые замки. Захватить блокировку чтения на карте, ключ поиска, вернуть, если имеется, в противном случае снять блокировку чтения на карте, захватить блокировку записи на карте, проверить заполнитель, захватить блокировку чтения заполнителя, если имеется, в противном случае создать заполнитель, захватить блокировку записи, вставить на карту, снимите блокировку записи на карте, создайте значение, замените заполнитель на значение, снимите блокировку записи с заполнителя.
Оценка : Вставка заполнителя в карту при построении значения гарантирует, что только один поток попытается его выполнить, что решает проблемы Попытки 2. Однако этот дизайн оставляет открытым вопрос: что форма этот заполнитель должен взять? Ответ не тривиален. Во-первых, если он содержит блокировку и потоки ожидают его, становится трудно его удалить (Как вы знаете, что никто не удерживает блокировку заполнителя? Вы можете захватить ее, конечно, но тогда вы держите ее так , опять же, вы не можете удалить его). Во-вторых, могут быть поиски ключей, для которых не существует значения на диске. Если мы вставим заполнитель для каждой попытки поиска (даже для тех, которые в конечном итоге потерпят неудачу), то кто будет их очищать? Наконец, с двумя уровнями блокировок код становится довольно уродливым: сначала мы берем блокировку чтения, проверяем, затем получаем блокировку записи, перепроверяем, и нам нужно сделать это как на уровне карты, так и на уровне отдельных заполнителей (легко чтобы создать тупики, я могу это засвидетельствовать).
Этот маленький ребенок продолжает появляться, и я не могу найти элегантное решение. Попытка 3 - самая близкая мне попытка удовлетворить мои условия производительности, но она уродлива и подвержена ошибкам, когда вы приступаете к грязным деталям. Буду очень признателен за любые идеи или предложения.