Java Как убедиться, что совместная запись данных влияет на несколько читателей - PullRequest
0 голосов
/ 21 ноября 2018

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

Допустим, у меня есть тысячи итераций, и все итерации имеют две фазы:

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

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

Я предполагаю, что когда люди говорят о кеше или кэшировании в этомв контексте они означают кэш процессора (исправьте, если я ошибаюсь).

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

Что еще я могу использовать в этом случае?

Сейчас у меня есть несколько идей, но я понятия не имею, насколько они дорогостоящие (скорее всего, они):

  1. создать новые рабочие потоки для всех итераций

  2. в синхронизированном блоке сделать копию массива (может быть размером до 195 КБ) для каждого потока передначинается новая итерация

  3. Я читал о ReentrantReadWriteLock, но не могу понять, как это связано с кэшированием.Может ли блокировка чтения получить принудительно обновить кэш считывателя?

1 Ответ

0 голосов
/ 24 ноября 2018

То, что я искал, было упомянуто в «Учебнике Java по параллелизму», мне просто пришлось заглянуть глубже.В данном случае это был класс AtomicIntegerArray.К сожалению, он недостаточно эффективен для моих нужд.Я запускаю несколько тестов, возможно, им стоит поделиться.

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

Я использовал размер целочисленного массива 50000 и повторял каждый метод тестирования 100 раз, затем усреднял результаты.Тесты чтения выполняют 50000 случайных (ish) чтений.Результаты показывают приблизительное время одного доступа для чтения / записи.Тем не менее, это нельзя назвать точным измерением, но я считаю, что оно дает хорошее представление о затратах времени на различные методы доступа.Однако на разных процессорах или с разными числами эти результаты могут быть совершенно разными в зависимости от разных размеров кэша и тактовых частот.

Таким образом, получаются следующие результаты:

  1. Время заполнения с помощью набора равно: 15.922673ns
  2. Время заполнения с помощью lazySet: 4.5303152ns
  3. Время атомарного чтения: 9.146553ns
  4. Синхронизированное время чтения: 57.858261399999996ns
  5. SingleВремя заполнения с резьбой: 0,2879112 нс
  6. Время чтения с одной резьбой: 0,3152002 нс
  7. Время неизменного копирования: 0,2920892 нс
  8. Время неизменного чтения: 0,650578 нс

В точках 1 и 2 показан результат записи в массив AtomicIntegerArray с последовательной записью.В какой-то статье я рассказал о хорошей эффективности метода lazySet (), поэтому я хотел его протестировать.Обычно метод over () выполняется более чем в 4 раза, однако разные размеры массива показывают разные результаты.

Точки 3 и 4 показывают разницу между «атомарным» доступом и синхронизированным доступом (синхронизированный геттер)к одному элементу массива через случайное чтение (ish) четырьмя различными потоками одновременно.Это ясно указывает на преимущества «атомарного» доступа.

Поскольку первые четыре значения выглядели шокирующе высокими, я действительно хотел измерить время доступа без многопоточности, поэтому я получил результаты пунктов 5 и 6. Япытался копировать и модифицировать методы из предыдущих тестов, чтобы сделать код максимально близким.Конечно, могут быть оптимизации, на которые я не могу повлиять.

Тогда просто из любопытства я придумываю пункты 7. и 8., которые имитируют неизменный доступ.Здесь один поток создает массив (путем последовательной записи) и передает его ссылку на другой поток, который выполняет случайный (ish) доступ к нему для чтения.

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

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

...