КАК ПОБЕДИТЬ ОЧЕРЕДИ С ПОМОЩЬЮ ОЧЕРЕДИ Брязгин А.С., Жукова А.А. Руководитель Рощева Т.А. (доцент кафедры теоретической механики, к.ф.-м.н.) ФГАОУ ВПО "Уральский Федеральный Университет имени первого Президента России Б.Н. Ельцина" Екатеринбург, Россия В работе моделируется параллельная/многопоточная обработка данных. HOW TO WIN QUEUES WITH HELP OF THE QUEUE Bryazgin A.S., Zhukova A.A. Head Roscheva T.A. (Associate Professor, Department of Theoretical Mechanics., PhD) FGAOU VPO "Ural Federal University named after the first President of Russia Boris Yeltsin" Ekaterinburg, Russia Parallel/multi-threaded processing is simulated in the article. Чего больше всего не хватает людям сейчас? Возможно, кто-то ответит, что ему не хватает денег, но мы больше склоняемся к варианту, что всем катастрофически не достаёт времени, бездельники не в счёт. В данном вопросе нам поможет Гай Юлий Цезарь, который, как известно, мог заниматься несколькими делами одновременно — согласитесь, удобно. Применим его опыт в повседневной жизни. Все мы попадали в ситуацию, когда очередь в столовую на большаке настолько велика, что вставать в неё, кажется и смысла не имеет — всё равно за 45 минут до кассы не доберёшься, или когда, чтобы зайти в ГУК надо изрядно потолкаться в дверном проходе. А представьте, если бы в столовой работало 4 кассы или на входе в ГУК, были бы открыты все двери — поток людей разделился бы и прошёл намного быстрее. Конечно же, мы не первые, кто заметил, что делать одновременно несколько дел — очень продуктивно. По принципу распараллеливания потока работают многоядерные компьютеры, в них каждое ядро занимается своим процессом, не мешая другому, и благодаря этому люди могут одновременно скачивать фильм, слушать музыку и писать курсовую работу, не чувствуя, что их компьютер надрывается от тяжести поставленных задач. Для своей программы мы создали такую модель: несколько источников (заказчиков), несколько исполнителей и одна очередь, которая является главным организатором работы. Дело в том, что мало создать многопоточный процесс, необходимо его ещё и правильно организовать, при параллельной работе нескольких потоков может возникать множество разнообразных проблем: исполнители могут одновременно пытаться взять одну и ту же работу, или наоборот — первый ждёт, пока второй выполнит работу, а второй ждёт, пока первый выполнит свою, в результате — все стоят. В общем смысле, все проблемы упираются в то, что в программе есть неделимые отрезки, во время выполнения которых нельзя внедряться в процесс и начинать даже пытаться делать что-нибудь другое. В жизни это может выглядеть примерно так: на первом курсе студентам читают динамику точки в курсе лекций по теоретической механике, где необходимо интегрировать дифференциальные уравнения, однако в их курсе математики до темы дифференциальные уравнения ещё очень далеко, то есть нарушена последовательность получения знаний. В связи с этим мы создали отдельный управляющий и организовывающий поток — очередь, которая следит за правильностью последовательности действий всех участников программы. В главе всего стоит очередь. Она объявляет заказчикам, что готова получать работу (в нашем случае работа заключается просто в ожидании), заказчики отдают её всю имеющуюся у них работу, затем очередь объявляет исполнителям, что у неё есть для них работа, и последовательно раздаёт её. Затем очередь проверят, всю ли работу раздали заказчики, всю ли получили исполнители, и если это условие выполнено, она закрывается. Цель задачи: Моделирование параллельной/многопоточной обработки данных. Развернутое описание задачи: Задаётся начальный класс (WorkInfo), содержащий информацию необходимую для выполнения работы, в моем случае это время выполнения работы. Необходимо написать классы WorkSource (источник) и WorkPerformer (исполнитель), которые будут записывать и выполнять эти задания путем добавления их в очередь (WorkQueue). Экземпляры классов WorkSource и WorkPerformer должны работать каждый в своем потоке. Алгоритм работы программы: WorkQueue: 1 Информируем о начале работы очереди. Проверяем на присутствие источников, исполнителей, очереди заданий. 2 Информируем о запуске всех источников. Запускаем источники. 3 Информируем о запуске всех исполнителей. Запускаем исполнителей. 4 Дожидаемся конца работы исполнителей, конца работы, конца работы исполнителей. 5 Оповещаем о конце всей работы. WorkPerformer: 1 Информируем что начал работу поток-исполнитель 2 Берем задание из очереди a) Смотрим, есть ли на данный момент в очереди работа. b) Если есть – берем. b.i Проверяем выполнена ли вся работа b.ii Если выполнена – оповещаем всех исполнителей b.iii Переходим в п.3. c) Если нет – смотрим, есть ли на данный момент источники. d) Если есть – ждем добавления работы и переходим в п.2a. e) Если нет – заканчиваем работу всех потоков-исполнителей. 3 Выполняем задание a) Информируем о начале выполнения задания в соответствующем потоке b) Выполняем задание c) Информируем о конце выполнения задания в соответствующем потоке 4 Проверяем флаг, показывающий выполнена ли вся работа a) Если не выполнена – п.2 b) Если выполнена – п.5 5 Информируем о конце работы соответствующего потока-исполнителя. Заканчиваем работу. WorkSource 1 Информируем о начале работы потока-источника 2 Для всех работ в списке a) Добавляем в очередь b) Засыпаем 3 Информируем о конце работы потока-источника Текст программы: 1. Класс Main: package threadkurs; import java.util.ArrayList; public class Main { public static void main(String[] args) { WorkQueue WQ = new WorkQueue(10); ArrayList<WorkInfo> listWI = new ArrayList<WorkInfo>(); for(int i = 0; i<3; i++){ listWI.add(new WorkInfo(2000)); } ArrayList<WorkSource> listWS = new ArrayList<WorkSource>(); for(int i = 0; i<2; i++){ listWS.add(new WorkSource(1000,listWI)); } ArrayList<WorkPerformer> listWP = new ArrayList<WorkPerformer>(); for(int i = 0; i<3; i++){ listWP.add(new WorkPerformer()); } WQ.regListWP(listWP); WQ.regListWS(listWS); WQ.startWork(); } } 2. Класс WorkInfo: package threadkurs; public class WorkInfo{ int time; WorkInfo(){ time = 500; } WorkInfo(int time){ this.time = time; } } 3. Класс WorkQueue: package threadkurs; import java.util.ArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class WorkQueue{ private ReentrantLock lockW = new ReentrantLock(); private Condition condW = lockW.newCondition(); private ReentrantLock lockWP = new ReentrantLock(); private Condition condWP = lockWP.newCondition(); private Integer waitTime = new Integer(10);//время ожидания private ArrayList<WorkInfo> listW = new ArrayList<WorkInfo>();//список заданий private Integer iterW = new Integer(0);//"указатель" на работу, которую необходимо отдать private ArrayList<WorkPerformer> listWP = new ArrayList<WorkPerformer>();//список исполнителей private Integer cntActivWP = new Integer(0);//количество активных выполняторов private ArrayList<WorkSource> listWS = new ArrayList<WorkSource>();//список источников private Integer cntActivWS = new Integer(0);//количество активных добавляторов WorkQueue(){ this.waitTime = 10; } WorkQueue(int waitTime){ if(waitTime > 0){this.waitTime = waitTime;} else{System.out.printf("Время ожидания должно быть положительно!(Оставлено значение по умолчанию: %d) \n ",this.waitTime);} } void startWork(){ System.out.println("-- НАЧАЛО ВСЕЙ РАБОТЫ --"); if(listWS.isEmpty() && listW.isEmpty()){ System.out.println("Пустая очередь и нет источников работы!"); return; } if(listWP.isEmpty()){ System.out.println("Нет исполнителей"); return; } System.out.println("-- ЗАПУСК ДОБАВЛЯТОРОВ --"); for(WorkSource WS : listWS){ WS.start(); } System.out.println("-- ЗАПУСК ВЫПОЛНЯТОРОВ --"); for(WorkPerformer WP : listWP){ WP.start(); } for(WorkSource WS : listWS){ try{ WS.join(); }catch(Exception Ex){System.out.println("Исключение при ожидании источников!");} }//ждем завершение работы всех источников lockW.lock(); try{ while(iterW != listW.size()){ try{ condW.await(waitTime,TimeUnit.SECONDS); }catch(Exception Ex){System.out.println("Исключение при ожидании конца работы!");} } }finally{ lockW.unlock(); }//ждем разбора всех заданий for(WorkPerformer WP : listWP){ try{ WP.join(); }catch(Exception Ex){System.out.println("Исключение при ожидании исполнителей!");} }//хдем конца работы всех исполнителей System.out.println("-- КОНЕЦ ВСЕЙ РАБОТЫ --"); } WorkInfo giveWork() throws StopWorkExeption{ WorkInfo newWI = null; lockW.lock(); tryGetWork : try{ getWork : while(true){ if(iterW < listW.size()){ newWI = listW.get(iterW); iterW++; break getWork; } else{ if(cntActivWS == 0){ throw new StopWorkExeption(); }else{ try{ if(condW.await(waitTime, TimeUnit.SECONDS)){ throw new StopWorkExeption(); } }catch(InterruptedException Ex){} } } } } finally{ condW.signalAll(); lockW.unlock(); } if(workIsDone()){callAllWP();} return newWI; } void regWP(WorkPerformer WP){ WP.WQ = this; lockWP.lock(); try{ listWP.add(WP); cntActivWP++; }finally{ condWP.signalAll(); lockWP.unlock(); } } void regListWP(ArrayList<WorkPerformer> listWP){ for(WorkPerformer WP : listWP){ regWP(WP); } } void regWS(WorkSource WS){ WS.WQ = this; lockW.lock(); try{ listWS.add(WS); cntActivWS++; }finally{ condW.signalAll(); lockW.unlock(); } } void regListWS(ArrayList<WorkSource> listWS){ for(WorkSource WS : listWS){ regWS(WS); } } boolean workIsDone(){//проверка на присутствие источников и работы boolean res = false; lockW.lock(); try{ res = (iterW >= listW.size() && cntActivWS == 0); } finally{ lockW.unlock(); } return res; } void startPerform(long ID){ System.out.printf("НАЧАЛ РАБОТУ ВЫПОЛНЯТОР (ID = %d)\n",ID); } void endPerform(long ID){ lockWP.lock(); try{ --cntActivWP;//говорим что закончили работу System.out.printf("ЗАКОНЧИЛ РАБОТУ ВЫПОЛНЯТОР (ID = %d)\n",ID); }finally{ condWP.signalAll(); lockWP.unlock(); } } void startSource(long ID){ System.out.printf("НАЧАЛ РАБОТУ ДОБАВЛЯТОР (ID = %d)\n",ID); } void endSource(long ID){ lockW.lock(); try{ --cntActivWS; }finally{ condW.signalAll(); lockW.unlock(); } System.out.printf("ЗАКОНЧИЛ РАБОТУ ДОБАВЛЯТОР (ID = %d)\n",ID); } void addWork(WorkInfo WI, long ID){ lockW.lock(); try{ System.out.printf("++Добавление работы (ID = %d)\n",ID); listW.add(WI); }finally{ condW.signalAll(); lockW.unlock(); } } //оповещение всех исполнителей о конце всей работы void callAllWP(){ lockWP.lock(); try{ for(WorkPerformer WP : listWP){ WP.endMyPerform(); } } finally{ lockWP.unlock(); } } } 4. Класс WorkSource: package threadkurs; import java.util.ArrayList; public class WorkSource extends Thread{ private long threadID;//идентификатор данного потока WorkQueue WQ;//очередь private Integer time; //время сна между добавления заданий private ArrayList<WorkInfo> listW;//список работы WorkSource(Integer time, ArrayList<WorkInfo> listW){ this.time = time; this.listW = listW; } @Override public void run(){ threadID = Thread.currentThread().getId(); WQ.startSource(threadID); for(WorkInfo WI : listW){ addWork(WI); try{ Thread.sleep(time); }catch(Exception Ex){System.out.printf("Игнорирована попытка прерывания работы источника работ - добавляем дальше");} } WQ.endSource(threadID); } void addWork(WorkInfo WI){ WQ.addWork(WI, threadID); } } 5. Класс WorkPerformer: package threadkurs; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class WorkPerformer extends Thread{ ReentrantLock lockEnd = new ReentrantLock();//блокировка на флаг о выполнении всей работы Condition condEnd = lockEnd.newCondition(); long threadID;//идентификатор данного потока WorkInfo WI;//текущая работа WorkQueue WQ;//очередь boolean workIsDone = false;//флаг о выполнении всей работы @Override public void run(){ threadID = Thread.currentThread().getId(); WQ.startPerform(threadID); executeAll(); } void executeAll(){ getWI(); while(!isDone()){ execute(WI); getWI(); } } void execute (WorkInfo WI){ System.out.printf("->Начало выполнения работы(ID = %d)\n",threadID); try{ Thread.sleep(WI.time); }catch(Exception Ex){ System.out.printf("><Прерывание работы(ID = %d)\n",threadID); } System.out.printf("<-Конец выполнения работы(ID = %d) \n",threadID); } void getWI(){ try{ WI = WQ.giveWork(); }catch(StopWorkExeption Ex){ System.out.printf("StopWorkExeption (ID = %d) \n",threadID); } if(WI == null) endMyPerform(); } void endMyPerform(){ lockEnd.lock(); System.out.printf("endMyPerform (ID = %d) \n",threadID); try{ workIsDone = true; }finally{ lockEnd.unlock(); } } boolean isDone(){ boolean res = false; lockEnd.lock(); try{ res = workIsDone; }finally{ lockEnd.unlock(); } return res; } } 6. Класс StopWorkExeption: public class StopWorkExeption extends Exception{ } Тестовые данные: Имеем 2 источника, которые будут спать между добавлениями по 1 сек и добавят по 3 работы каждый (работа заключается в засыпании на 2 сек). Имеем 3 исполнителя, которые будут пытаться брать работу из очереди и выполнять ее. Имеем очередь, в которую регистрируем всех источников и исполнителей. Время ожидания у очереди ограничено - 0.01 сек. Результаты тестов (результат работы программы): 1) run: -- НАЧАЛО ВСЕЙ РАБОТЫ --- ЗАПУСК ДОБАВЛЯТОРОВ --- ЗАПУСК ВЫПОЛНЯТОРОВ -НАЧАЛ РАБОТУ ДОБАВЛЯТОР (ID = 8) НАЧАЛ РАБОТУ ВЫПОЛНЯТОР (ID = 10) НАЧАЛ РАБОТУ ДОБАВЛЯТОР (ID = 9) НАЧАЛ РАБОТУ ВЫПОЛНЯТОР (ID = 12) НАЧАЛ РАБОТУ ВЫПОЛНЯТОР (ID = 11) ++Добавление работы (ID = 8) ++Добавление работы (ID = 9) ->Начало выполнения работы(ID = 10) ->Начало выполнения работы(ID = 12) ++Добавление работы (ID = 8) ++Добавление работы (ID = 9) StopWorkExeption (ID = 11) endMyPerform (ID = 11) <-Конец выполнения работы(ID = 12) <-Конец выполнения работы(ID = 10) ->Начало выполнения работы(ID = 12) ->Начало выполнения работы(ID = 10) ++Добавление работы (ID = 8) ++Добавление работы (ID = 9) ЗАКОНЧИЛ РАБОТУ ДОБАВЛЯТОР (ID = 9) ЗАКОНЧИЛ РАБОТУ ДОБАВЛЯТОР (ID = 8) <-Конец выполнения работы(ID = 12) <-Конец выполнения работы(ID = 10) ->Начало выполнения работы(ID = 12) endMyPerform (ID = 10) ><Прерывание работы(ID = 12) <-Конец выполнения работы(ID = 12) endMyPerform (ID = 11) StopWorkExeption (ID = 12) endMyPerform (ID = 12) -- КОНЕЦ ВСЕЙ РАБОТЫ -BUILD SUCCESSFUL (total time: 4 seconds) Заключение: Результаты тестов соответствуют ожиданиям. Программа добавила всех источников и исполнителей в очередь. Исполнители взяли работу и добавили в очередь. Работа была выполнена в разных потоках. Программа работает правильно. Из чего можно сделать вывод о том, что способ решения проблемы, описанный нашей программой, готов для выполнения схожих задач и верен для взятой модели. Данная статья написана на основе курсовой работы по теме: «Многопоточная обработка данных» выполненной под руководством преподавателя Кукушкиной Е.В. на кафедре прикладной математики Уральского энергетического института УрФУ имени первого Президента России Б.Н. Ельцина. Список литературы: 1. Кей С. Хорстманн, Гари Корнелл “Java 2 седьмое издание” М.: Вильямс, 2007г. 2. Герберт Шилдт “Полное руководство Java” М.: Вильямс, 2012г. 3. Гудрич М.Т., Тамассия Р.; Структуры данных и алгоритмы в Java; Новое знание, 2003. - 672 c. 4. Мартин К. Соломон и др.; пер. Дранишников И.; Oracle. Программирование на языке Java; Лори, 2010. - 512 с. 5. Naftalin M., Wadler P.; Java Generics and Collections; O'Reilly, 2006. - 286 с.