Рубрики
Без рубрики

Подписка на одновременные обновления свойств

Параллелизм – это сложно. Я знаю, что это прописная истина, но это нужно было сказать! Я провел некоторое время в прошлом… С тегами параллелизм, java, открытый исходный код, подписчики.

Параллелизм – это сложно. Я знаю, что это прописная истина, но это нужно было сказать!

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

На первый взгляд, это казалось легкой задачей.

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

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

Поскольку атомарные ссылки поддерживают реквизит, я предположил, что реализация будет простой:

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

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

Асинхронная обработка этих событий означает, что нет никаких гарантий относительно порядка обработки.

Представьте себе следующий сценарий:

Events:
- set value A
- notify subscribers of value A
- set value B
- notify subscribers of value B

ForkJoinPool:
- notifies subscribers of value B
- notifies subscribers of value A

Outcome:
- the Prop's value is set to B
- but the subscribers were last sent A

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

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

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

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

  • Напишите тесты, чтобы воспроизвести ваш сценарий и ожидаемые результаты, затем запустите их несколько раз (подумайте о Junit + повторять до отказа, десятки тысяч раз).
  • Иногда вам нужна дополнительная информация для отладки проблем; у вас возникнет соблазн использовать System.out.print() , но это может ввести в заблуждение; это добавляет несколько миллисекунд задержки, которые могут полностью сорвать ваше воспроизведение, когда сценарий, который вы пытаетесь отладить, выполняется за наносекунды.
  • System.out.print() ложь: выходной поток буферизован и может быть напечатан в неправильном порядке; вызов System.out.flush() помогает, но не идеален и все еще страдает от проблемы порядков величины, описанной выше
  • Две независимые атомарные операции не подходят для одной атомарной операции; поначалу это противоречит здравому смыслу, но чем больше вы об этом думаете, тем больше смысла имеет то, что два потока, выполняющие одни и те же две операции, могут упорядочивать их 4 разными способами; найдите способ обновить оба значения за одну атомарную операцию
  • Синхронизация нескольких реквизитов означает, что обновления должны быть упорядочены; Я нашел одно решение: поставить в очередь операции обновления и передать их в Атомарная ссылка.updateAndGet() как Унарный оператор<Пара U> . U> . Этот метод использует weakCompareAndSet Энергозависимые эффекты памяти, указанные в JLS

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

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

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

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

Еда на вынос

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

Если вам понравилась эта статья и вы хотите прочитать ее еще больше, пожалуйста, подпишитесь на мою рассылку новостей ; Я рассылаю ее каждые несколько недель!

Оригинал: “https://dev.to/mihaibojin/subscribing-to-concurrent-property-updates-19pj”