Голодание (starvation)
Проблема полной недоступности ресурсов определённому потоку в связи с потреблением их другими потоками. Может проявляется при некорректном планирования выполнения (например различие в приоритетах на определённых системах), либо более частый случай - ошибки при проектировании и реализации. Если у одного потока будет выставлен очень высокий приоритет, а у другого низкий, то первый может потреблять существенную все вычислительные ресурсы, в то время как второй не получит их вовсе. К счастью, такое поведение практически невозможно на большинстве платформ поддерживаемых Java. В то время как ошибки при проектировании и реализации приводящие к проблеме голодания вполне реальны. Например ошибка некорректной обработки условия выхода из цикла выполняющегося при захваченной блокировке, которую ожидает другой поток.
Пример 12.
public class Starvation { public static void main(String[] args) { final ReentrantLock lock1 = new ReentrantLock(); Thread t = new Thread(new Runnable() { @Override public void run() { delay();//реализация аналогична DeadLock lock1.lock(); try { //критическая секция System.out.println("completed"); } finally { lock1.unlock(); } } }, "slave"); t.start(); lock1.lock(); try { while (!Thread.currentThread().isInterrupted()) { //условие выхода которое не наступает } } finally { lock1.unlock(); } System.out.println("finished"); } } |
Детектирование проблемы голодания не столь прямолинейно как в случае с взаимной блокировкой. Если ситуация подобна примеру, то можно легко вычислить не ожидающий поток с бесконечным циклом любой утилитой отображающей состояние процессов и потоков в операционной системе. Далее получить дамп потоков и определить место возникновения проблемы. Но если в такой ситуации внутри цикла поток будет блокироваться в ожидании события или просто отдавать рекомендацию планировщику (Thread.yield, Thread.sleep) уступить текущий квант, то определить её будет сложнее. При подобных условиях могут пригодиться как ручное наблюдение за стеками выполнения и состояниями потоков во времени, так и автоматическое профилирование приложения (может понадобиться длительное время сбора статистики).
Активная блокировка (livelock) .
Это зацикливание алгоритма в результате взаимодействия состоянием, которое в свою очередь может независимо изменяться извне. Условие существование изменяемого извне состояния - параллелизм (необязательно многопоточность, например, взаимодействие процессов с файловой системой). При активной блокировки поток не блокируется, а продолжает попытки выполнить полезное действие, но в результате некорректной обработки ошибки при его выполнение повторяет его снова. Например завершение транзакции в СУБД, которая не может быть выполнена по причине нарушения в работе сервера, но некорректный код трактует ошибку как временную и пытается повторить транзакцию. Ещё один классический пример из области сетевых технологий. Если два или более передающих устройства одновременно пытаются передать данные, то обнаруживая коллизию и повторяя попытки передачи через равные интервалы времени ни одно устройство не сможет ничего передать, так как каждый раз разделяемая среда будет передавать сигналы нескольких источников.
Помимо таких явных проблем блокирующих выполнение, как упомянутые выше в этой секции, существуют и менее критичные проблемы производительности и латентности (времени реакции). Наверное, наиболее распространённым их видом, связанным с производительностью, будет являться проблема со слабо гранулированными блокировками (coarse-grained lock) или их экстремальным вариантом - глобальными блокировками (global lock). Суть этой проблемы сводится к тому, что в результате больших размеров критической секции выполнение на этом участке фактически сериализуется (смотри 2.4 Блокировки). К проблемам латентности можно отнести ситуацию близкую к голоданию, когда какой-то поток практически не получает время центрального процессора или прочий ресурс, в результате чего растягивается время выполнения операции.
Заключение теоретической части.
В данном обзоре описывается, хотя бы очень кратко, большая часть основных вопросов связанных с поддержкой параллелизма в Java. Надеюсь, мне удалось передать наиболее важные аспекты этой проблемы в достаточном для понимания объеме.
Для реализации поставленной задачи я буду использовать описанные процессы и потоки, но мне нет необходимости использовать описанные в обзоре методы, так как задача стоит в оценке быстродействия вычислении функции при использовании разного количества потоков.
Алгоритм работы программы.
Согласно этому алгоритму напишем текст программы. Листинг программы приведем ниже.
Приложение 1.
import javax. swing.*; //подключение библиотеки вывода окна //на экран
public class Vine {
static int x=0,y=0,z=1; //инициализация общих аргументов //функции
//**********************************подсчет c использованием многопоточности
public static class ThreadTest implements Runnable {
long before1=0;long after1=0; //переменные подсчета времени
public void run() {
double calc; //переменная подсчета значений функ //ции
before1 = System.currentTimeMillis(); //установка значений переменной вре //мени «до»
for (int i=0; i<5000; i++) {
//подсчет значений функции
calc =(Math.sin(i*y)*Math.cos(z*2))+(Math.cos(i*y)*Math.sin(2*z));
//условие вывода на экран промежу //точного значения
if (i%1000==0) { //условие вывода промеж. знач
after1=System.currentTimeMillis(); //установка значений переменной вре //мени «после»
//вывод на экран промежуточного //значения
System.out.format("%s count %d fixing %.4f time %d ms\n",getName(),(i/1000),calc,(after1-before1));
}
}
}
}
public static String getName() {
return Thread.currentThread().getName(); //возвращение имени потока
}
//*********************************подсчет без использования многопоточности
public static void free() {
long before2=0;long after2=0; //переменные подсчета времени
before2= System.currentTimeMillis(); //установка значений переменной вре //мени «до»
double calc; //переменная подсчета значений функ //ции
for ( int i=0; i<5000; i++) {
//подсчет значений функции
calc =(Math.sin(i*y)*Math.cos(z*2))+(Math.cos(i*y)*Math.sin(2*z));
if (i%1000==0) { //условие вывода промеж. знач
after2=System.currentTimeMillis(); //установка значений переменной вре //мени «после»
//вывод на экран промежуточного //значения
System.out.format("count %d fixing: %.4f time %d ms\n",(i/1000),calc,(after2-before2));
}
}
}
public static void main(String[] args) { //функция main()
//вывод запроса на экран
String input_x = JOptionPane.showInputDialog("Введите количество потоков:");
x = Integer.parseInt(input_x); //считывание значения в переменную
//вывод запроса на экран
String input_y = JOptionPane.showInputDialog ("Введите аргумент A:") ;
y = Integer.parseInt(input_y); //считывание значения в переменную
//вывод запроса на экран
String input_z = JOptionPane.showInputDialog ("Введите аргумент B:") ;
z = Integer.parseInt(input_z); //считывание значения в переменную
//вывод на консоль текста
System.out.format("<Выполнение программы без использования многопоточности>\n");
free(); //вызов процедуры подсчета значения //функции без многопоточности
//вывод на консоль текста
System.out.format("<Выполнение программы c использованием многопоточности>\n");
// Подготовкапотоков
Thread t[] = new Thread[x];
for (int i=0; i<t.length; i++) {
t[i]=new Thread(new ThreadTest(),"Thread "+i);
}
// Запускпотоков
for (int i=0; i<t.length; i++) {
t[i].start();
System.out.format("%s <starter> \n",t[i].getName());
}
System. exit(0); //завершение работы программы
}
}
Проанализируем результат выполнения программы. Результат выполнения программы приведен ниже.
Приложение 2.
Результат 1.
При работе 5 потоков.
<Выполнение программы без использования многопоточности>
count 0 fixing: 0,9093 time 0 ms
count 1 fixing: 0,9974 time 16 ms
count 2 fixing: 0,5394 time 16 ms
count 3 fixing: -0,9384 time 16 ms