У меня есть 32 машинных потока и один ConcurrentHashMap<Key,Value> map
, который содержит много ключей. Key
определил общедоступный метод visit()
. Я хочу visit()
каждый элемент карты ровно один раз, используя доступную вычислительную мощность и, возможно, какой-то пул потоков.
Что я мог бы попробовать:
- Я мог бы использовать метод
map.keys()
. РезультирующийEnumeration<Key>
можно было бы повторить с помощьюnextElement()
, но поскольку вызовkey.visit()
очень короткий, мне не удастся занять потоки. Перечисление по своей сути является однопоточным. - Вместо этого я мог бы использовать синхронизированный
HashSet<Key>
, вызвать методtoArray()
и разделить работу над массивом на все 32 потока. Я серьезно сомневаюсь в этом решении, так как методtoArray()
, скорее всего, будет узким местом для одного потока. - Я мог бы попытаться наследоваться от
ConcurrentHashMap
, заполучить экземпляры его внутреннегоSegment<K,V>
, попытаться сгруппировать их в 32 группы и работать с каждой группой отдельно. Хотя это звучит как хардкорный подход. - или подобная магия с
Enumeration<Key>
.
В идеале:
- В идеале
ConcurrentHashMap<Key, Value>
определял бы методkeysEnumerator(int approximatePosition)
, который мог бы отбросить мне перечислитель, в котором отсутствуют примерно первые 1/32 элемента, т.е.map.keysEnumerator(map.size()/32)
Я пропустил что-то очевидное? Кто-нибудь сталкивался с подобной проблемой раньше?
ИЗМЕНИТЬ
Я попробовал профилировать, чтобы увидеть, действительно ли эта проблема повлияет на производительность на практике. Поскольку в данный момент у меня нет доступа к кластеру, я использовал свой ноутбук и попытался экстраполировать результаты на больший набор данных. На моей машине я могу создать 2 миллиона ключей ConcurrentHashMap, и для его повторения требуется около 1 секунды, вызывая метод visit()
для каждого ключа. Предполагается, что программа масштабируется до 85 миллионов ключей (и более). Процессор кластера немного быстрее, но итерация по всей карте все равно занимает около 40 секунд. Теперь несколько слов о логике работы программы. Представленная логика является последовательной, то есть ни один поток не может перейти к следующему шагу, пока все потоки на предыдущем шаге не будут завершены:
- Create the hash map, create the keys and populate the hash map
- Iterate over entire hash map visiting all the keys.
- Do some data shuffling which is parallel insertions and deletions.
- Repeat step 2 and 3 a few hundred times.
Этот логический поток означает, что 40-секундная итерация будет повторяться несколько сотен раз, скажем, 100. Это дает нам чуть больше часа, потраченного только на посещение узлов. С набором из 32 параллельных итераторов это может сократиться до нескольких минут, что является значительным улучшением производительности.
Теперь несколько слов о том, как работает ConcurrentHashMap
(или как я думаю, что это работает). Каждый ConcurrentHashMap
состоит из сегментов (по умолчанию 16). Каждая запись в хэш-карту синхронизируется в соответствующем сегменте. Итак, скажем, мы пытаемся записать два новых ключа k1 и k2 в хеш-карту и что они будут разрешены как принадлежащие одному и тому же сегменту, скажем, s1. Если их попытаются записать одновременно, один из них получит блокировку первым и будет добавлен раньше, чем другой. Какова вероятность того, что два элемента будут принадлежать одному и тому же сегменту? В случае, если у нас есть хорошая хеш-функция и 16 сегментов, это 1/16.
Я считаю, что ConcurrentHashMap
должен иметь метод concurrentKeys()
, который будет возвращать массив Enumerations, по одному на каждый сегмент. У меня есть несколько идей, как добавить его в ConcurrentHashMap
через наследование, и я дам вам знать, если у меня получится. На данный момент решение, по-видимому, заключается в создании массива ConcurrentHashMaps и предварительном хэшировании каждого ключа для разрешения одного члена такого массива. Я также поделюсь этим кодом, как только он будет готов.
ИЗМЕНИТЬ
Та же проблема на другом языке: