Таким образом, для каждой условной переменной существует своя очередь заблокированных задач, и активизировать задачи можно из конкретной очереди, «адресуя» действие 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