Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Sb97573

.pdf
Скачиваний:
4
Добавлен:
13.02.2021
Размер:
685.79 Кб
Скачать

Многопоточность в Java

Создать поток в Java-приложении можно, расширяя стандартный класс java.lang.Thread. В этом классе определены несколько конструкторов и около 30 методов, позволяющих создавать потоки и управлять их состоянием. Среди этих методов есть метод run(), который в стандартной реализации класса Thread ничего не делает и который необходимо определить при создании собственного потока. Этот вновь созданный метод run() во вновь созданном классе-наследнике и определит последовательность действий вновь созданного потока:

public class GetPut extends Thread { int period;

String text;

GetPut(int p, String s) { period = p;

text = s;

}

public void run() { while(true){

System.out.print(text);

try{

sleep(period); }catch(InterruptedException e){

e.printStackTrace();

}

}

}

public static void main(String[] args) { new GetPut(1000,"Thread put ").start(); new GetPut(1500,"Thread get ").start();

}

}

В приведенном примере класс GetPut, расширяющий класс Thread, задает тип потока и определяет его последовательность действий в методе public void run().

Поток этого типа в бесконечном цикле выводит на экран строку text с периодом period. Далее в методе main() создаются 2 потока типа GetPut,

21

которые запускаются методом start(). Результат работы приведенного примера будет выглядеть следующим образом:

Thread put Thread get Thread put Thread get Thread put . . . .

Синхронизированные методы, оператор synchronized

В Java существует возможность создать класс, объекты которого будут обладать свойствами монитора. Такая возможность обеспечивается оператором synchronized, позволяющим создавать синхронизированные методы класса:

class Example {

public synchronized void doActionOne() {

. . .

}

public synchronized void doActionTwo() {

. . .

}

public void doActionThree() {

. . .

}

}

Объект класса Example, который можно создать firstObject = new Example(),

будет обладать свойством взаимоисключающего доступа к методам doActionTwo и doActionTwo. Другими словами, поток, который вызовет один из них, заблокирует возможность доступа к этим методам из других потоков. При этом доступ к методу doActionThree, который не является синхронизированным, будет свободным. В таком мониторе для блокировки вызывающей задачи может быть использован метод wait(), а для возобновления ее действий, по аналогии с signal(), может быть использован метод notify(). Обратим внимание на тот факт, что в таком случае создаются две очереди заблокированных задач – одна из них служит для организации взаимного исключения доступа к монитору, а в другую помещаются потоки, заблокированные при вызове метода wait().

Приведем пример, который часто называют «Почтовый ящик». Создаются две задачи, одна из которых – «отправитель» (объект класса Sender) посылает строку в «почтовый ящик» (объект класса Box), а другая –

22

«получатель» (объект класса Recipient) забирает эту строку. При этом «получатель» ждет, пока строка не появится в ящике, а «отправитель» не посылает строку, если ящик полон. При этом один извещает другого при выполнении своего действия:

public class PostBox {

public static void main(String[] args){ Box b = new Box();

new Recipient(b, 1500).start(); new Sender(b, 1000).start();

}

}

class Box {

String message; boolean full = false;

public synchronized String receive(){ while (!full) {

try {

System.out.println("Receive wait" + full); wait();

} catch (InterruptedException e) { e.printStackTrace();

}

}

full = false; notify(); return message;

}

public synchronized void send(String s){ while (full)

try{

System.out.println("Send wait"); wait();

}catch(InterruptedException e){ e.printStackTrace();

}

message = s;

23

full = true; notify();

}

}

class Recipient extends Thread { int period;

Box box; String S;

Recipient(Box b, int p) { period = p;

box = b;

}

public void run() { while (true) {

S = box.receive(); System.out.println("Recipient got - " + S); try{

sleep(period);

} catch (InterruptedException e) { e.printStackTrace();

}

}

}

}

class Sender extends Thread { int period;

Box box;

Sender(Box b, int p) { box = b;

period = p;

}

public void run() { while (true) {

box.send("Be Happy!"); System.out.println("Sender sent"); try {

24

sleep(period);

} catch (InterruptedException e) { e.printStackTrace();

}

}

}

}

Вприведенном примере имеется только одно условие, которое может принимать 2 значения – «ящик полон» (full = true) и «ящик пуст» (full = false). Решение задачи свелось к созданию монитора с двумя процедурами, в каждой из которых производится проверка этого условия, блокировка (в случае необходимости) задачи, которая эту процедуру выполняет (wait), и освобождение «альтернативной» задачи (notify), которая может быть заблокирована вызовом wait() в другой процедуре.

Вопределении монитора, данном Хоаром, функции, блокирующие и освобождающие поток, связывались с конкретной условной переменной (wait(с), signal(с)). Следовательно, определив несколько условных переменных, можно блокировать и освобождать задачи, связывая эти действия с конкретными условиями:

monitor : МониторХоара

S1, S2: condition; Procedure_1 {

if (условие_a) wait(S1);

. . .

if (условие_b) wait(S2);

}

Procedure_2 { signal(S1);

}

Procedure_3 { signal(S2);

}

End МониторХоара.

25

Таким образом, для каждой условной переменной существует своя очередь заблокированных задач, и активизировать задачи можно из конкретной очереди, «адресуя» действие signal(S1) или signal(S2) к очереди конкретной условной переменной S1 или S2. Реализация монитора в Java, использующая синхронизированные методы, не предоставляет механизма условной переменной. В таком мониторе для действия wait() создается только одна, «неименованная» очередь, и работа происходит только с ней. Нет возможности «привязывать» операции wait() и notify() к конкретному условию, что может создавать определенные неудобства разработчику многониточных приложений.

6.СРЕДСТВА СИНХРОНИЗАЦИИ В ОПЕРАЦИОННЫХ СИСТЕМАХ

Вкаждой многозадачной операционной системе пользователю предоставляется программный интерфейс – API (Application Programming Interface) для работы с функциями (примитивами) ядра. При этом как у пользователей, так и у разработчиков ОС возникает естественное стремление

куниверсализации API. Наиболее наглядно это представляется на примере системы UNIX, а точнее – систем UNIX, так как начиная с 70-х гг. ХХ в. появляется множество разновидностей UNIX-подобных систем. В 90-х гг. появляется первоначальный вариант стандарта POSIX (IEEE Portable Operating System Interface for Computer Environment), который был предназначен для стандартизации механизмов взаимодействия

пользовательского приложения и операционной системы такого класса. В настоящее время стандарт POSIX представляет собой набор из более чем 30 стандартов, описывающих различные аспекты ОС. Многозадачность здесь представляется на уровне процессов и потоков в стандарте POSIX.1c, Threads extensions (IEEE Std 1003.1c-1995), определяющем программный интерфейс для управления, планирования и синхронизации потоков. Реализация данного стандарта на языке Си обычно осуществляется в библиотеке Pthread, определяющей набор типов данных и функций (заголовочный файл pthread.h), имеющих приставку pthread. Интерфейс для управления потоками включает в себя более 10 типов данных и около 20 функций. Для примера рассмотрим некоторые из них.

26

Создание потока

Поток создается вызовом следующей функции:

int pthread_create( pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void* ), void* arg ),

где thread – указатель на переменную, в которую будет помещен идентификатор потока (ID), устанавливаемый системой при его создании; attr

– указатель на атрибутную запись (дескриптор) потока, при значении NULL значения дескриптора устанавливаются по умолчанию; void* (*start_routine)(void*) – указатель на функцию с единственным параметром, код которой выполняется в потоке; arg – указатель на единственный аргумент, передаваемый в функцию потока, в случае отсутствия параметра устанавливается NULL.

Вызов pthread_create(&t, NULL, &func, NULL) создает и запускает поток, ID которого сохраняется в переменной t; атрибутная запись потока сохраняет стандартные значения; при работе выполняются действия, описанные в функции func, в которую значение параметра не передается.

Вызов pthread_t pthread_self(void) возвращает идентификатор вызвавшего потока.

Ожидание завершения потока

Следующий вызов заставляет ждать завершения потока: int pthread_join( pthread_t thread, void** value_ptr ),

где thread – ID потока, до завершения которого будет приостановлена работа потока, вызвавшего pthread_join; value_ptr – указатель на переменную, в которую будет помещен статус потока с идентификатором ID после его окончания.

#include <unistd.h> void* func (void* args){

. . .

}

int main()(int argc, char *argv[]) { pthread_t thread_id;

pthread_create(&thread_id, NULL, &func, NULL);

. . .

pthread_join(thread_id, NULL);

}

27

В данном примере поток, созданный функцией main() и вызвавший pthread_join(thread_id, NULL), приостанавливает свое выполнение до того момента, пока не закончится выполнение потока с идентификатором thread_id.

Функция main() является функцией процесса, в котором реализован поток thread_id. Завершение main() раньше потока thread_id может привести к принудительному (и, возможно, нежелательному) завершению последнего. Таким образом, pthread_join осуществляет синхронизацию завершения двух потоков.

Семафор

Стандарт POSIX предусматривает 2 вида семафоров – именованные семафоры, предназначенные для синхронизации процессов, и неименованные семафоры. Неименованные семафоры служат для синхронизации потоков, работающих в адресном пространстве одного процесса.

Далее рассмотрим функции для работы только с неименованными семафорами, описание которых представляется в заголовочном файле semaphore.h (обратим внимание на то, что семафоры не относятся к средствам, описанным в pthread.h).

Следующая функция инициализирует семафор:

int sem_init( sem_t * sem, int pshared, unsigned value ),

где sem – указатель на описатель семафора; pshared – при значении параметра NULL семафор размещается в памяти процесса и может использоваться только его потоками, при значении O_MP_OBJ семафор размещается в общей памяти и может быть использован всеми процессами; value –

начальное значение семафора.

 

 

 

 

Р-операция над семафором

sem

выглядит

следующим

образом:

int sem_wait( sem_t * sem).

 

 

 

 

V-операция над семафором

sem

выглядит

следующим

образом:

int sem_post( sem_t * sem).

 

 

 

 

Следующая функция позволяет проверить состояние семафора без блокировки: int sem_trywait( sem_t * sem ) – значение семафора sem проверяется и уменьшается на 1 аналогично операции sem_wait, однако если это значение меньше 1, то функция возвращает –1 и выполнение потока продолжается. Другими словами, если семафор свободен, то он

28

захватывается, как и в sem_wait, а если занят, то блокировка не происходит, а возвращается код ошибки.

Получение значения семафора производится следующей функцией: int sem_getvalue(sem_t* sem, int* value).

Рассмотрим следующий пример:

#include <pthread.h> #include <semaphore.h> void f(pthread_t id) {

printf("Thread %d called this function\n", id);

}

sem_t sem;

void* thread_2(void* arg) { pthread_t id = pthread_self(); for(;;) {

printf("Thread %d is working\n", id ); sem_wait(&sem);

f(id); sem_post(&sem); sleep(1);

}

return 0;

}

int main(int argc, char *argv[]) { pthread_t thr_2; sem_init(&sem, NULL, 1); pthread_t id = pthread_self();

pthread_create(&thr_2, NULL, &thread_2, NULL); for(;;) {

printf("Thread %d is working\n", id ); sem_wait(&sem);

f(id); sem_post(&sem); sleep(1);

}

printf("Main thread stop\n"); pthread_join(thr_2, NULL);

29

return EXIT_SUCCESS;

}

В приведенном примере обеспечивается взаимное исключение

выполнения функции f(), вызываемой из двух потоков.

 

 

Мьютекс

 

Следующая

функция

инициализирует

мьютекс:

int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr ),

где mutex – указатель на описатель мьютекса; attr – указатель на описатель атрибутов мьютекса; при значении NULL используются атрибуты по умолчанию.

Следующая функция захватывает мьютекс, если он свободен, или

блокирует

поток,

если

мьютекс

занят:

int pthread_mutex_lock(pthread_mutex_t* mutex).

 

 

Следующая функция освобождает занятый мьютекс (может только

владелец): int pthread_mutex_unlock(pthread_mutex_t* mutex).

 

Следующая

функция

проверяет состояние

мьютекса и при

этом не

блокирует поток: int pthread_mutex_trylock(pthread_mutex_t* mutex); функция захватывает мьютекс, если он свободен, а если мьютекс занят, то работа потока все равно продолжается, при этом функция возвращает код ошибки

EBUSY.

Если мьютекс с именем mutex свободен и один из потоков, выполнив действие pthread_mutex_lock(&mutex), захватил его, то этот поток становится его владельцем. Если затем другой поток пытается освободить этот мьютекс, выполняя вызов pthread_mutex_unlock(&mutex), то такой вызов должен приводить к ошибочной ситуации. Однако в разных операционных системах такая ситуация разбирается по-разному. Чаще всего попытка освобождения

мьютекса «не владельцем» приводит к

тому, что

действие

int i = pthread_mutex_unlock(&mutex) возвращает

некоторый код

ошибки,

например [EPERM], реакция на который должна быть предусмотрена в программе.

Еще одну особенность реализации можно определить как «возможность рекурсивного захвата». Рассмотрим следующую последовательность операций над мьютексом с именем mutex, выполняемую в одном потоке:

pthread_mutex_lock(&mutex);

// 1

. . .

 

30

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]