Примечание. Я уже обращался к этой проблеме в другом сообщении SO - Использование семафора внутри вложенного Java-параллельного потока действий может DEADLOCK. Это ошибка?, но название этого сообщения предполагало, что проблема связана с использованием семафора, что несколько отвлекло обсуждение. Я создаю это, чтобы подчеркнуть, что вложенные циклы могут иметь проблемы с производительностью, хотя обе проблемы, вероятно, являются общей причиной (и, возможно, потому, что мне потребовалось много времени, чтобы выяснить эту проблему). (Я не считаю это дубликат, потому что он подчеркивает другой симптом, но если вы просто удалите его).
Проблема: Если вы вставляете два цикла Java 8 stream.parallel(). forEach, а все задачи независимы, неактивны и т.д. - за исключением того, что они отправляются в общий пул FJ, то вложенность параллельный цикл внутри параллельного цикла выполняет гораздо слабее, чем вложение последовательного цикла внутри параллельного цикла. Еще хуже: если операция, содержащая внутренний цикл, синхронизирована, вы получите DEADLOCK.
Демонстрация проблемы производительности
Без "синхронизированного" вы все равно можете наблюдать проблему с производительностью. Вы найдете демо-код для этого: http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachTest.java (см. JavaDoc там для более подробного описания).
Наша настройка выглядит следующим образом: у нас есть вложенный поток .parallel(). forEach().
- Внутренний цикл является независимым (безстоящим, без помех и т.д. - за исключением использования общего пула) и потребляет всего 1 секунду в худшем случае, а именно, если обрабатывается последовательным.
- Половина задач внешнего цикла потребляет за 10 секунд до этого цикла.
- Половина потребляет 10 секунд после этого цикла.
- Следовательно, каждая нить потребляет 11 секунд (в худшем случае). * У нас есть логическое значение, которое позволяет переключать внутренний цикл с параллельного() на последовательный().
Теперь: отправив 24 внешних цикла в пул с parallelism 8, мы ожидаем 24/8 * 11 = 33 секунды в лучшем случае (на 8-ядерном или более высоком компьютере).
Результат:
- С внутренним последовательным циклом: 33 секунды.
- С внутренним параллельным циклом: > 80 секунд (у меня было 92 секунды).
Вопрос: Можете ли вы подтвердить это поведение? Это чего-то можно ожидать от структуры? (Я немного более осторожен сейчас с утверждением, что это ошибка, но я лично считаю, что это связано с ошибкой в реализации ForkJoinTask. Замечание: я разместил это в concurrency -interest (см. http://cs.oswego.edu/pipermail/concurrency-interest/2014-May/012652.html), но до сих пор я не получил подтверждения оттуда).
Демонстрация тупика
Следующий код DEADLOCK
// Outer loop
IntStream.range(0,numberOfTasksInOuterLoop).parallel().forEach(i -> {
doWork();
synchronized(this) {
// Inner loop
IntStream.range(0,numberOfTasksInInnerLoop).parallel().forEach(j -> {
doWork();
});
}
});
где numberOfTasksInOuterLoop = 24
, numberOfTasksInInnerLoop = 240
, outerLoopOverheadFactor = 10000
и doWork
- некоторый безгосударственный процессор.
Вы найдете полный демо-код на http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachAndSynchronization.java (см. JavaDoc там для более подробного описания).
Ожидается ли такое поведение? Обратите внимание, что в документации по параллельным потокам Java не упоминается проблема с вложением или синхронизацией. Кроме того, не упоминается тот факт, что оба используют общий fork-join-pool.
Обновление
Еще один тест по проблеме производительности можно найти на http://svn.finmath.net/finmath%20experiments/trunk/src/net/finmath/experiments/concurrency/NestedParallelForEachBenchmark.java - этот тест выполняется без какой-либо операции блокировки (нет Thread.sleep и не синхронизирован). Я собрал еще несколько замечаний здесь: http://christian-fries.de/blog/files/2014-nested-java-8-parallel-foreach.html
Обновление 2
Похоже, что эта проблема и более серьезный DEADLOCK с семафорами были исправлены в Java8 u40.