Минимизация перераспределения контекста Java Thread Context

У меня есть приложение Java, работающее на 32-разрядном виртуальном процессоре Sun/Solaris 10 (x86)/Nahelem с 8 ядрами (2 потока на ядро).

Конкретная утилита в приложении - это ответ на какое-либо внешнее сообщение. В моей тестовой среде производительности, когда я готовлю и отправляю ответ в том же потоке, который получает внешний вход, я получаю около 50 us преимущество, чем когда я передаю сообщение отдельному потоку для отправки ответа. Я использую ThreadPoolExecutor с SynchronousQueue, чтобы выполнить передачу обслуживания.

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

Ответ 1

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

Конечно, если только один поток может получать запросы, то привязка этого потока для вычисления ответа будет означать, что вы не можете принимать другие запросы. В зависимости от того, что вы делаете, вы можете использовать асинхронный IO (и т.д.), Чтобы избежать модели "поток на запрос", но это значительно сложнее IMO и все еще заканчивается переключением контекста потока.

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

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

Ответ 2

50us звучит несколько высоко для эстафетной передачи обслуживания, IME (Solaris 10/Opteron) LBQ обычно находится в диапазоне 30-35us, тогда как LTQ (LinkedTransferQueue) примерно на 5US быстрее. Как указано в других ответах SynchronousQueue, может иметь тенденцию быть немного медленнее, потому что предложение не возвращается до тех пор, пока не будет получен другой поток.

В соответствии с моими результатами Solaris 10 заметно медленнее, чем Linux, который видит времена < 10us.

Это действительно зависит от нескольких вещей, при максимальной нагрузке

  • сколько запросов в секунду вы обслуживаете?
  • сколько времени обычно требуется для обработки запроса?

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

Ответ 3

Есть ли причина, по которой вы не используете LinkedBlockingQueue, чтобы ваш производитель мог поставить в очередь пару элементов вместо SynchronousQueue? По крайней мере, у вас есть очередь с 1 элементом, чтобы вы могли стать лучше parallelism.

Какова скорость процесса "подготовки" к "отклику"? Можете ли вы использовать пул потоков, чтобы иметь несколько потоков, обрабатывающих ответы, если они слишком дороги?

Ответ 4

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

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