Предположим, у меня есть следующий код, который я не буду оптимизировать, распространяя рабочую нагрузку на несколько ядер процессора моего ПК:
double[] largeArray = getMyLargeArray();
double result = 0;
for (double d : largeArray)
result += d;
System.out.println(result);
В этом примере я мог бы распределить работу, выполненную внутри цикла for, в течение нескольких потоков и убедиться, что все потоки завершены, прежде чем приступать к печати result
. Поэтому я придумал что-то похожее на это:
final double[] largeArray = getMyLargeArray();
int nThreads = 5;
final double[] intermediateResults = new double[nThreads];
Thread[] threads = new Thread[nThreads];
final int nItemsPerThread = largeArray.length/nThreads;
for (int t = 0; t<nThreads; t++) {
final int t2 = t;
threads[t] = new Thread(){
@Override public void run() {
for (int d = t2*nItemsPerThread; d<(t2+1)*nItemsPerThread; d++)
intermediateResults[t2] += largeArray[d];
}
};
}
for (Thread t : threads)
t.start();
for (Thread t : threads)
try {
t.join();
} catch (InterruptedException e) { }
double result = 0;
for (double d : intermediateResults)
result += d;
System.out.println(result);
Предположим, что длина largeArray
массива делится на nThreads
. Это решение работает правильно.
Тем не менее, я столкнулся с проблемой, что вышеупомянутая потоковая обработка for-loops очень много в моей программе, что вызывает много накладных расходов из-за создания и сборки мусора потоков. Поэтому я смотрю на изменение моего кода с помощью ThreadPoolExecutor
. Затем потоки, дающие промежуточные результаты, будут повторно использоваться в следующем выполнении (суммирование в этом примере).
Поскольку я сохраняю свои промежуточные результаты в массиве размера, который должен быть известен заранее, я думал об использовании пула потоков фиксированного размера. Однако у меня возникли проблемы с тем, чтобы сообщить нить о том, в каком месте в массиве должен храниться его результат. Должен ли я определить свой собственный ThreadFactory
?
Или мне лучше использовать массив ExecutorService
созданный методом Executors.newSingleThreadExecutor(ThreadFactory myNumberedThreadFactory)
?
Обратите внимание, что в моей реальной программе очень сложно заменить double[] intermediateResults
на что-то другого типа. Я бы предпочел решение, которое ограничивается созданием правильного типа пула потоков.
Однако у меня возникли проблемы с тем, чтобы сообщить
thread
о том, в каком месте вarray
должен храниться его результат. Должен ли я определить свой собственныйThreadFactory
?
Нет необходимости в этом. Интерфейсы, используемые исполнителями (Runnable
and Callable
), управляются потоками, и вы можете передавать любые аргументы в реализации, которые вы хотите передать (например, массив, индекс начала и конечный индекс).
ThreadPoolExecutor
- действительно хорошее решение. Также посмотрите на FutureTask
если у вас есть runnables с результатами.
ExecutorService
предоставляет вам API для получения результата из пула потоков через интерфейс Future
:
Future<Double> futureResult = executorService.submit(new Callable<Double>() {
Double call() {
double totalForChunk = 0.0;
// do calculation here
return totalForChunk;
}
});
Теперь все, что вам нужно сделать, это отправить задания (экземпляры Callable
) и дождаться, когда результат будет доступен:
List<Future<Double>> results = new ArrayList<Double>();
for (int i = 0; i < nChunks; i++) {
results.add(executorService.submit(callableTask));
}
Или даже проще:
List<Future<Double>> results = executorService.invokeAll(callableTaskList);
Остальное легко, перебирать results
и собирать всего:
double total = 0.0;
for (Future<Double> result : results) {
total += result.get(); // this will block until your task is completed by executor service
}
Сказав это, вам все равно, сколько потоков у вас есть в службе исполнителя. Вы просто отправляете задания и собираете результаты, когда они доступны.
Вам было бы лучше создавать "рабочие" потоки, которые берут информацию о работе, которая будет выполняться из очереди. Тогда ваш процесс должен был создать первоначально пустой WorkQueue, а затем создать и запустить рабочие потоки. Каждый рабочий поток забирал свою работу из очереди, выполнял работу и помещал работу в "завершенную" очередь для мастера, чтобы забрать и обработать.
Runnable
должен знать, в каком потоке он находится, а он, в свою очередь, знает, в каком месте массива он должен храниться. Таким образом, я могу сказать, что поток 1 хранится на месте 1 массива. На самом деле это мышление просто странно, поскольку потоки параллельны, и нет интуитивно понятного способа, которым поток 1 по праву помечается как «1». Я могу просто указать 5 (или вообще: количество ядер, которые у меня есть).Runnable
s хранит каждое в уникальном месте в массиве.Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
- хорошая вещь?