#java #multithreading
#java #многопоточность
Вопрос:
Я пытаюсь изучить многопоточное программирование, и у меня есть несколько вопросов о подходе, который необходимо будет предпринять.
Итак, в моем конкретном случае я хочу создать программу, которая переименовывает 1000 файлов, и я думал создать рабочий класс:
public class Worker implements Runnable {
private List<File> files ;
public Worker(List<File> f){
files = f;
}
public void run(){
// read all files from list and rename them
}
}
а затем в основном классе сделать что-то вроде:
Worker w1 = new Worker(..list of 500 files...) ;
Worker w2 = new Worker(..list of the other 500 files...) ;
Thread t1 = new Thread(w1,"thread1");
Thread t2 = new Thread(w2,"thread2");
t1.start();
t2.start();
Запуск этого не вызывает у меня проблем с параллелизмом, поэтому мне не нужен синхронизированный код, но я не уверен, что это правильный подход …?
Или я должен создать только один экземпляр Worker() и передать весь список файлов 1000, и позаботиться о том, чтобы независимо от того, сколько потоков обращаются к объекту, thew не получит тот же файл из списка?
то есть :
Worker w1 = new Worker(..list of 1000 files...) ;
Thread t1 = new Thread(w1,"thread1");
Thread t2 = new Thread(w1,"thread2");
t1.start();
t2.start();
Как мне поступить здесь?
Комментарии:
1.
brings me no concurrency issues...
Не между t1 и t2, а как насчет t1 и основного потока (соответственно t2)?2. Первая мера: предположение о том, что большее количество потоков, выполняющих дисковый ввод-вывод (переименование файлов) на одном диске одновременно, быстрее, обычно неверно. Базовая файловая система предоставляет гарантии, которые могут потребовать использования блокировок (например, блокировки каталога при атомарном переименовании файла), что, в свою очередь, означает, что все / большинство операций синхронизированы. Дополнительное преимущество (немного) ускорения работы с многопоточным кодом не всегда перевешивает простоту / удобство обслуживания однопоточного кода.
3. вы правы, Ваноекель, но я нашел в этой программе хорошую возможность получить некоторые навыки многопоточности 🙂
Ответ №1:
Первый подход, который вы назвали, правильный. Вам нужно создать два Worker
, так как каждый рабочий будет работать с разным списком файлов.
Worker w1 = new Worker(..list of 500 files...) ; // First List
Worker w2 = new Worker(..list of the other 500 files...) ; // Second List
Thread t1 = new Thread(w1,"thread1");
Thread t2 = new Thread(w2,"thread2");
t1.start();
t2.start();
Здесь все просто: два разных потока с загрузкой в 500 файлов будут выполняться одновременно.
Ответ №2:
Более типичным и масштабируемым подходом является один из следующих:
- создайте коллекцию (вероятно, массив или список) из N потоков для выполнения работы
- используйте пул потоков, например, из Executors.newFixedThreadPool(N)
Вы также можете использовать шаблон потребителя-производителя, в котором потоки извлекаются из общего пула задач. Это позволяет естественным образом сбалансировать работу — вместо по существу жесткого кодирования один поток обрабатывает 500 задач, а другой столько же.
Подумайте, в конце концов, что произойдет, если все ваши файлы большего размера окажутся в корзине, обрабатываемой потоком2? Первый поток завершен / простаивает, а второй поток должен выполнять всю тяжелую работу.
Подход к объединению производителей и потребителей заключается в том, чтобы сбрасывать всю работу (сгенерированную производителем) в пул задач, а затем потребители (ваши рабочие потоки) откусывают небольшие кусочки (например, один файл) за раз. Этот подход приводит к тому, что оба потока остаются занятыми в течение одинаковой продолжительности.
Ответ №3:
При изучении многопоточного программирования одним из важных выводов является то, что поток — это не задача. Предоставляя потоку часть списка элементов для обработки, вы на полпути, но следующий шаг приведет вас дальше: построение задачи таким образом, чтобы ее могло выполнить любое количество потоков. Для этого вам нужно будет ознакомиться с java.util.concurrent
классами. Это полезные инструменты, помогающие создавать задачи.
Приведенный ниже пример отделяет задачи от потоков. Он используется AtomicInteger
для обеспечения того, чтобы каждый поток выбирал уникальную задачу, и он использует CountDownLatch
, чтобы знать, когда вся работа выполнена. В примере также показана балансировка: потоки, которые выполняют задачи, которые выполняются быстрее, выполняют больше задач. Приведенный пример ни в коем случае не является единственным решением — существуют и другие способы сделать это, которые могут быть быстрее, проще, удобнее в обслуживании и т.д..
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class MultiRename implements Runnable {
public static void main(String[] args) {
final int numberOfFnames = 50;
MultiRenameParams params = new MultiRenameParams();
params.fnameList = new ArrayList<String>();
for (int i = 0; i < numberOfFnames; i ) {
params.fnameList.add("fname " i);
}
params.fnameListIndex = new AtomicInteger();
final int numberOfThreads = 3;
params.allDone = new CountDownLatch(numberOfThreads);
ExecutorService tp = Executors.newCachedThreadPool();
System.out.println("Starting");
for (int i = 0; i < numberOfThreads; i ) {
tp.execute(new MultiRename(params, i));
}
try { params.allDone.await(); } catch (Exception e) {
e.printStackTrace();
}
tp.shutdownNow();
System.out.println("Finished");
}
private final MultiRenameParams params;
private final Random random = new Random();
// Just to show there are fast and slow tasks.
// Thread with lowest delay should get most tasks done.
private final int delay;
public MultiRename(MultiRenameParams params, int delay) {
this.params = params;
this.delay = delay;
}
@Override
public void run() {
final int maxIndex = params.fnameList.size();
int i = 0;
int count = 0;
while ((i = params.fnameListIndex.getAndIncrement()) < maxIndex) {
String fname = params.fnameList.get(i);
long sleepTimeMs = random.nextInt(10) delay;
System.out.println(Thread.currentThread().getName() " renaming " fname " for " sleepTimeMs " ms.");
try { Thread.sleep(sleepTimeMs); } catch (Exception e) {
e.printStackTrace();
break;
}
count ;
}
System.out.println(Thread.currentThread().getName() " done, renamed " count " files.");
params.allDone.countDown();
}
static class MultiRenameParams {
List<String> fnameList;
AtomicInteger fnameListIndex;
CountDownLatch allDone;
}
}
Комментарии:
1. «поток — это не задача». Верно, но вы можете создать систему, которая запускает каждую задачу в своем собственном новом потоке (не спрашивайте меня, откуда я знаю!). Однако это не очень разумная идея, если стоимость выполнения задачи существенно не превышает стоимость создания и уничтожения потоков, и даже тогда это может быть неразумно.
2. @jameslarge Вот где
ThreadPool
появляется a. Однажды я переработал программу, чтобы использовать один пул потоков и заменитьnew Thread(task).start()
наThreadPool.execute(task)
: это было относительно легко сделать, и увеличение производительности было заметным. Единственное, на что следует обратить внимание, это количество похожих задач, выполняемых одновременно, например. ограничьте количество задач, работающих с локальными файлами, до 8 (или любого другого подходящего) вместо того, чтобы позволять пулу потоков расти до сотен потоков, выполняющих задачи, используя один и тот же (ограниченный) ресурс.