Я хочу використовувати Stream
паралельну обробку гетерогенного набору віддалено зберігаються файлів JSON невідомого числа (кількість файлів наперед не відома). Файли можуть різнитися за розмірами, від 1 запису JSON на файл до 100 000 записів у деяких інших файлах. Запис JSON в цьому випадку означає самодостатній об'єкт JSON, представлений як один рядок у файлі.
Я дуже хочу використовувати для цього Streams, і тому я реалізував це Spliterator
:
public abstract class JsonStreamSpliterator<METADATA, RECORD> extends AbstractSpliterator<RECORD> {
abstract protected JsonStreamSupport<METADATA> openInputStream(String path);
abstract protected RECORD parse(METADATA metadata, Map<String, Object> json);
private static final int ADDITIONAL_CHARACTERISTICS = Spliterator.IMMUTABLE | Spliterator.DISTINCT | Spliterator.NONNULL;
private static final int MAX_BUFFER = 100;
private final Iterator<String> paths;
private JsonStreamSupport<METADATA> reader = null;
public JsonStreamSpliterator(Iterator<String> paths) {
this(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths);
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths) {
super(est, additionalCharacteristics);
this.paths = paths;
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths, String nextPath) {
this(est, additionalCharacteristics, paths);
open(nextPath);
}
@Override
public boolean tryAdvance(Consumer<? super RECORD> action) {
if(reader == null) {
String path = takeNextPath();
if(path != null) {
open(path);
}
else {
return false;
}
}
Map<String, Object> json = reader.readJsonLine();
if(json != null) {
RECORD item = parse(reader.getMetadata(), json);
action.accept(item);
return true;
}
else {
reader.close();
reader = null;
return tryAdvance(action);
}
}
private void open(String path) {
reader = openInputStream(path);
}
private String takeNextPath() {
synchronized(paths) {
if(paths.hasNext()) {
return paths.next();
}
}
return null;
}
@Override
public Spliterator<RECORD> trySplit() {
String nextPath = takeNextPath();
if(nextPath != null) {
return new JsonStreamSpliterator<METADATA,RECORD>(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths, nextPath) {
@Override
protected JsonStreamSupport<METADATA> openInputStream(String path) {
return JsonStreamSpliterator.this.openInputStream(path);
}
@Override
protected RECORD parse(METADATA metaData, Map<String,Object> json) {
return JsonStreamSpliterator.this.parse(metaData, json);
}
};
}
else {
List<RECORD> records = new ArrayList<RECORD>();
while(tryAdvance(records::add) && records.size() < MAX_BUFFER) {
// loop
}
if(records.size() != 0) {
return records.spliterator();
}
else {
return null;
}
}
}
}
Проблема, яка у мене виникає, полягає в тому, що спочатку Потік прекрасно паралелізується, з часом найбільший файл залишається обробляти в один потік. Я вважаю, що проксимальна причина добре зафіксована: сплітератор "неврівноважений".
Більш конкретно, видається, що trySplit
метод не викликається після певного моменту Stream.forEach
життєвого циклу, тому додаткова логіка розподілу невеликих партій наприкінці trySplit
рідко виконується.
Зверніть увагу, як усі сплітератори, що повернулися з trySplit, діляться одним paths
ітератором. Я подумав, що це дійсно розумний спосіб збалансувати роботу між усіма розбірниками, але цього було недостатньо для досягнення повного паралелізму.
Я хотів би, щоб паралельна обробка проходила спочатку через файли, а потім, коли ще кілька великих файлів залишилося розщеплювати, я хочу паралелізувати між собою фрагменти інших файлів. Такий був намір else
блоку в кінці 2007 року trySplit
.
Чи існує легкий / простий / канонічний спосіб вирішити цю проблему?
Long.MAX_VALUE
викликає надмірне та непотрібне розщеплення, тоді як будь-яка оцінка, окрім як Long.MAX_VALUE
спричиняє подальше розщеплення, зупиняється, вбиваючи паралелізм. Повернення поєднання точних оцінок, схоже, не призводить до інтелектуальних оптимізацій.
AbstractSpliterator
але переосмислюєте, trySplit()
що є поганим комбо для нічого іншого, крім того Long.MAX_VALUE
, як ви не адаптуєте оцінку розміру в trySplit()
. Після trySplit()
цього оцінку розміру слід зменшити на кількість розділених елементів.