Министерство образования и науки Российской Федерации
Московский государственный технический университет им. Н.Э. Баумана
Факультет "Информатика и системы управления"
Кафедра ИУ-3
Методические указания к лабораторной работе
“Организация параллельных вычислений в многопроцессорных вычислительных системах»
по курсу "Распределенные информационные системы”
Составили: Недашковский В.М.,
Бабкин П.С.
Галькова Е.А.
Груздев О.В.
"Утверждаю"
Зав. кафедрой ИУ-3
д.т.н., профессор
Девятков В. В.
Москва 2011 г.
ОГЛАВЛЕНИ
1. Цель работы. 3
2. Задание для домашней подготовки. 3
3. Задание на лабораторную работу. 3
4. Контрольные вопросы 3
5. Список литературы 4
6. ПРИЛОЖЕНИЕ 1 5
7. ПРИЛОЖЕНИЕ 2 (Практическая часть) 14
1. Цель работы. 3
2. Задание для домашней подготовки. 3
3. Задание на лабораторную работу. 3
4. Контрольные вопросы 3
5. Список литературы 3
6. ПРИЛОЖЕНИЕ 1 3
7. ПРИЛОЖЕНИЕ 2 (Практическая часть) 3
Цель работы.
Изучение и исследование студентами организации параллельных вычислений в многопроцессорных вычислительных системах изучить возможность использования потоков в java на простом примере перемножения матриц заданной и регулируемой размерности
Задание для домашней подготовки.
( Теоретические сведения, необходимые для подготовки к лабораторной работе)
Изучить краткие теоретические сведения по организации потоков в java (Приложение 1).
Задание на лабораторную работу.
В данной лабораторной работе студент должен последовательно выполнить следующие пункты:
Изучить краткие теоретические сведения по организации потоков в java (Приложение 1).
Ознакомиться с механизмом организации потоков в исходном коде программы (Приложение 2).
Дополнить исходный программный код реализацией функции умножения матриц по заданным входным параметрам, используя спецификацию параметров функции (Приложение 2).
Дописать реализацию потоков при перемножении матриц с вариационным количеством строк и столбцов и разделением матриц по трем потокам (Приложение 2).
Варианты выполнения четвертого задания
Реализовать многопоточное перемножение матриц на C/C++.(Первая группа)ш
Реализовать многопоточное перемножение матриц на C#.(Вторая группа)
Контрольные вопросы
Список литературы
Приложение 1
Использование многопоточного вычисления дает возможность разбивать задачи требующие большое количество ресурсов.
Определение потока в Java
В Java термин “поток” может обозначать две разные вещи:
Экземпляр класса java.lang.Thread
Поток выполнения
Экземпляр Thread – это обычный объект. Подобно другим объектам в Java, он имеет переменные и методы, а живет и умирает в куче. Поток выполнения (thread of execution) – это отдельный процесс (“легковесный” процесс), имеющий свой собственный стэк вызовов. В Java для каждого потока существует один стэк вызовов. Даже если вы явно не создаете потоков в вашей программе, они там все равно есть.
Метод main(), например, выполняется в потоке, который называется “главным” (main thread), и если вы посмотрите на стэк вызовов, то увидите, что метод main() стоит первым в стеке, т.е. находится в самом низу.
Поток main
Как только вы создадите новый поток, создастся новый стэк, в который будут помещаться записи о вызовах методов, совершенных из этого нового потока.
JVM от Sun, начиная с версии 1.2 мапит Java-потоки на нативные потоки операционной системы один-к-одному, однако у JVM свой планировщик потоков, который не зависит от планировщика операционной системы под которой работает JVM. Одно следует знать точно: когда дело доходит до потоков, то здесь нельзя давать ни каких гарантий. Нельзя точно определить как будут выполняться параллельные потоки. Ответственность за это полностью лежит на планировщике JVM.
В Java есть два вида потоков: потоки-демоны (daemon threads) и пользовательские потоки (user threads). Здесь мы будем рассматривать в основном user threads. Разница между этими двумя типами потоков в том, что JVM завершает выполнение программы когда все пользовательские потоки завершат свое выполнение. Как только завершил свое выполнение последний пользовательский поток, JVM остановится не зависимо от того, в каком состоянии находятся потоки-демоны.
Создание потоков
Поточность в Java начинается с создания экземпляра java.lang.Thread. В этом классе мы найдем методы для управления потоками: для создания, запуска, и приостановки потоков. Следующие методы самые популярные:
start()
yield()
sleep()
run()
Весь экшн происходит в методе run(). В него помещается код, который требуется выполнить в отдельном потоке.
public void run() {
// код для выполнения в отдельном потоке
}
Из метода run() конечно же можно вызывать другие методы, а так как для отдельного потока создался новый стэк вызовов, то в этом новом стеке, первым методом будет метод run(). Определить и инстанциировать поток можно двумя способами:
Расширить класс java.lang.Thread
Реализовать интерфейс Runnable
Предпочтительным способом является реализация интерфейса Runnable. Расширить Thread – это просто, но этот способ не является хорошей практикой ООП. Почему? Потому что в Java нет множественного наследования и расширяя Thread, вы не можете сделать этот класс подклассом другого класса. Поэтому, лучше всего разработать класс, который реализует интерфейс Runnable.
Определение потока
Хоть в предыдущем разделе мы и выяснили что расширение Thread это не good OOP practice, но такой способ определения потока все же существует и заслуживает рассмотрения.
Расширение java.lang.Thread
Простейший путь определить поток – расширить Thread, переопределить метод run() и поместить в нем весь код, который нужно выполнить в отдельном потоке:
package ru.topcode.threadomaniac;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Очень важная работа выполняется в MyThread");
}
}
Вы можете свободно использовать перегрузку метода run():
package ru.topcode.threadomaniac;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Очень важная работа выполняется в MyThread");
}
public void run(String s) {
System.out.println("Строка из метода run: " + s);
}
}
Однако, не забывайте, что перегруженные run() игнорируются классом Thread. Класс Thread ожидает запуска именно run() без аргументов. Если вы вызовете run(String s), то вызов метода попадет в тот же стек вызовов, в котором лежит вызывающий его метод, т.е. отдельный поток не создастся.
Реализация интерфейса java.lang.Runnable
Реализация интерфейса Runnable дает возможность расширить любой другой класс и в то же время определить поведение, которое будет выполняться в отдельном потоке.
package ru.topcode.threadomaniac;
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Очень важная работа выполняется в MyRunnable");
}
}
Независимо от того, какой механизм вы выбрали, вы получили некоторый код, который может быть запущен. Теперь давайте посмотрим как запустить поток.
Инстанцирование потока
Каждый поток начинается с создания экземпляра Thread. Независимо от того, расширяли вы Thread или реализовывали Runnable, для выполнения работы, нужно иметь экземпляр Thread.
Если вы расширили класс Thread, то инстанцирование выполняется также как инстанцирование обычного объекта:
MyThread t = new MyThread();
Если вы реализовывали Runnable, вам все равно придется создать экземпляр класса Thread.
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
Объяснить это можно так: вместо того, чтобы комбинировать в одном классе и поток и выполняемую работу (код), мы разделяем логику на два класса – класс Thread для потоко-зависимого кода и реализация Runnable для описания работы, которая должна выполняться в отдельном потоке. Другими словами: Thread – это работник, а Runnable – это работа.
Один экземпляр Runnable можно передать нескольким объектам Thread:
MyRunnable r = new MyRunnable();
Thread foo = new Thread(r);
Thread bar = new Thread(r);
Thread bat = new Thread(r);
Это означает что несколько потоков будут делать одну и ту же работу. Значит эта работа будет выполнена несколько раз.
Класс Thread тоже реализует Runnable, а значит в конструктор Thread можно передать экземпляр Thread:
Thread t = new Thread(new MyThread());
Кроме конструктора по умолчанию и конструктора, принимающего экземпляр Thread, в классе Thread есть и другие перегруженные конструкторы. Вот список всех конструкторов Thread:
Thread()
Thread(String name)
Thread(Runnable runnable)
Thread(Runnable runnable, String name)
Thread(ThreadGroup g, Runnable runnable)
Thread(ThreadGroup g, Runnable runnable, String name)
Thread(ThreadGroup g, String name)
Итак, вы сделали себе экземпляр Thread и он знает что нужно выполнять метод run(). Но почему ничего не происходит? На данный момент мы имеем всего лишь обычный Java-объект типа Thread. Это еще не поток исполнения. Для того чтобы создать новый поток исполнения и новый стэк вызовов, нам нужно запустить поток.
Когда поток инстанцирован, но не запущен, говорят что он находится в состоянии new (новый). На этом этапе поток еще не считается живым (alive). После вызова метода start() экземпляра потока, поток переходит в состояние alive (живой). То что поток живой не означает что метод run() уже начал выполняться! Поток считается мертвым (dead) после того, как метод run() закончит выполняться. Метод isAlive() – лучший способ определить запущен ли поток и не завершил ли он уже выполняться. Часто при отладке используется метод getState().
Запуск потока
Пришло время запустить поток. Это настолько просто, что врядли заслуживает отдельного раздела:
t.start();
Что происходит после старта потока? А происходит следующее:
Стартует новый поток выполнения (с новым стэком вызовов).
Поток переходит из состояния new (новый) в состояние работоспособный (runnable).
Когда поток получает шанс выполниться, он вызывает метод run().
Запомните, что мы вызываем метод start() экземпляра класса Thread. Следующий пример продемонстрирует все что мы рассмотрели выше:
package ru.topcode.threadomaniac;
public class Starter {
public static void main(String[] args) {
FooRunnable r = new FooRunnable();
Thread t = new Thread(r);
t.start();
}
}
class FooRunnable implements Runnable {
public void run() {
for (int x = 1; x < 6; x++) {
System.out.println("Runnable running");
}
}
}
Еще раз заметьте, что для старта потока, нужно вызывать метод start() экземпляра класса Thread. Если вы вызовете метод run() из вашего класса, который реализует интерфейс Runnable или даже если вы вызовете run из класса, расширяющего Thread, то ничего страшного не произойдет. Не возникнет ни каких исключительных ситуаций относящихся к потокам. Метод просто выполнится, но новый поток не создастся! Метод выполнится в том же потоке, из которого был запущен! Следующий код не запускает новый поток выполнения:
Thread t = new Thread();
t.run(); // Так можно, но новый поток не создастся.
Так что же произойдет если запустить несколько потоков? Давайте рассмотрим пример запуска нескольких потоков. В следующем примере инстанцируются новые именованные потоки (потоки, которым мы явно присвоили имена). Имена здесь мы присвоили потокам чтобы проследить какой поток выполняется в данный момент времени.
package ru.topcode.threadomaniac;
public class Starter {
public static void main(String[] args) {
NameRunnable nr = new NameRunnable();
Thread t = new Thread(nr);
t.setName("Поток1");
t.start();
}
}
class NameRunnable implements Runnable {
public void run() {
System.out.println("NameRunnable запущен");
System.out.println("Выполняется "
+ Thread.currentThread().getName());
}
}
В результате получим следующий вывод в консоль:
NameRunnable запущен
Выполняется Поток1
Здесь, чтобы получить имя выполняющегося потока, мы использовали метод getName() экземпляра класса Thread. Статический метод Thread.currentThread() возвращает ссылку на текущий выполняемый поток.
Даже если вы явно не указываете имя потока, оно у него все равно будет. Закомментируйте строку
t.setName("Поток1");
и запустите программу. Вы получите следующий вывод в консоль:
NameRunnable запущен
Выполняется Thread-0
Так как мы получаем имя текущего потока с помощью статического метода Thread.currentThread(), то можно также получить имя главного потока – main.
Вот наглядное представление того, что происходит со стеками во время работы многопоточного приложения:
Выполнение многопоточного приложения
Запуск и выполнение нескольких потоков
package ru.topcode.threadomaniac;
public class Starter {
public static void main(String[] args) {
NameRunnable nr = new NameRunnable();
Thread one = new Thread(nr);
Thread two = new Thread(nr);
Thread three = new Thread(nr);
one.setName("Первый");
two.setName("Второй");
three.setName("Третий");
one.start();
two.start();
three.start();
}
}
class NameRunnable implements Runnable {
public void run() {
for (int x = 1; x <= 3; x++) {
System.out.println("Запущен "
+ Thread.currentThread().getName()
+ ", x равен " + x);
}
}
}
вывод в консоль:
Запущен Третий, x равен 1
Запущен Третий, x равен 2
Запущен Третий, x равен 3
Запущен Первый, x равен 1
Запущен Первый, x равен 2
Запущен Первый, x равен 3
Запущен Второй, x равен 1
Запущен Второй, x равен 2
Запущен Второй, x равен 3
В такой последовательности выполнялись потоки в этот раз. Однако, такое же поведение не гарантируется при следующем запуске программы. Также не гарантируется что когда поток начал выполняться, он будет продолжать выполняться пока не закончится его метод run(). В данном случае произошло именно так: каждый цикл успел полностью выполниться без прерываний. Запустив эту же программу еще раз получили следующий вывод:
Запущен Первый, x равен 1
Запущен Третий, x равен 1
Запущен Третий, x равен 2
Запущен Третий, x равен 3
Запущен Первый, x равен 2
Запущен Первый, x равен 3
Запущен Второй, x равен 1
Запущен Второй, x равен 2
Запущен Второй, x равен 3
В каждом отдельном потоке, порядок выполнения предсказуем, т.е., например, наши циклы всегда будут инкрементировать x и выводить в консоль сообщение, но порядок выполнения потоков не предсказуем. Велика вероятность того, что такой маленький и простой цикл всегда будет успевать выполняться до переключения на другой поток, но если увеличить длину цикла хотя бы до 400, можно увидеть, что потоки прерываются.
Так же не гарантируется что если поток1 начал выполняться первым, то он и закончит выполняться первым. Он может закончить свою работу и самым последним. Мы не контролируем планировщик потоков, поэтому не можем предсказать порядок выполнения, а соответственно, и порядок завершения потоков. Однако, существует способ сказать потоку чтобы не запускался пока какой-нибудь другой поток не закончился. Это можно сделать с помощью метода join(), который мы рассмотрим немного позже.
Когда метод run() потока завершился, поток перестает быть потоком исполнения, стек вызовов удаляется, а поток считается мертвым (dead). С этого момента поток – это обычный объект Thread. А можем ли мы снова запустить run() этого объекта? Нет! Если поток был запущен, то он никогда не может быть запущен повторно! Если у вас есть ссылка на поток, и вы вызовете его метод start() повторно, то получите IllegalThreadStateException. Запустить поток можно только из состояния new (новый), а состояние new он имеет только перед первым запуском. Если попытаться запустить поток из состояния runnable или dead, то получим IllegalThreadStateException.
До сих пор мы узнали о трех состояниях потока: новый (new), runnable (работоспособный), и dead (мертвый).
В дополнение к методам setName() и getName(), для идентификации потока можно еще использовать getId(). getId() возвращает положительное, уникальное число типа long и это число однозначно идентифицирует поток на протяжении всей его жизни.