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

u_course

.pdf
Скачиваний:
39
Добавлен:
04.06.2015
Размер:
1.87 Mб
Скачать

Средства разработки параллельных программм

81

и снимает блокировку сразу после изменения значения семафора, т.е. сигнал пробуждения потока формируется уже после снятия блокировки с семафора.

Рис. 3.7. Функции sem_trywait, sem_post() и pthread_mutex_trylock()

Хотя совместное обращение потоков к семафору не может привести к конфликту, следует понимать, что неверная логика использования семафоров

впрограмме, может привести не только к неверным вычислениям, но и к замораживанию нескольких потоков.

Кроме синхронизации потоков по использованию разделяемых ресурсов, часто необходима синхронизация по условию. В этом случае поток может быть приостановлен до наступления какого-нибудь события, отличного от положительности значения семафора. Ярким примером такого использования семафоров является задача о кольцевом буфере, подробно разобранная

вглаве 2. Ниже приведено решение этой задачи с использованием библиоте-

ки Pthread.

Задача о кольцевом буфере. Потоки производители и потребители разделяют кольцевой буфер, состоящий из 100 ячеек. Производители передают сообщение потребителям, помещая его в конец очереди буфера. Потребители сообщение извлекают из начала очереди буфера. Создать многопоточное приложение с потоками писателями и читателями. Предотвратить такие ситуации как, изъятие сообщения из пустой очереди или помещение сообщения

вполный буфер. При решении задачи использовать семафоры.

Задача обладает двумя критическими участками кода. Первая критическая секция связана с операциями чтения-записи нескольких потоков в общий буфер. Во втором случае проблема синхронизации определяется тем, что буфер являются конечным, запись должна производиться только в те ячейки, которые являются свободными или уже прочитаны потокамичитателями.

Для защиты первой критической секции воспользуемся двумя двоичными семафорами. Один двоичный семафор сделает возможным запись в буфер только для одного потока-писателя. Второй двоичный семафор сдела-

Средства разработки параллельных программм

82

ет возможным чтение из буфера только для одного потока-читателя. Операция чтения должна быть защищена, потому что она является и операцией записи, так как поток, прочитавший ячейку буфера, обязан ее как-то пометить. Иначе через определенное время выполнения программы, операция записи может стать невозможной или некорректной, в силу того, что буфер конечен. Операции чтения и записи могут выполняться одновременно, так как всегда происходят в разных ячейках.

Для реализации условной синхронизации воспользуемся двумя семафорами. Значение первого семафора показывает, сколько ячеек в буфере свободно. Ячейка свободна, когда в нее еще не осуществлялась запись или ячейка была прочитана. Значение второго семафора показывает, сколько ячеек в буфере занято. Естественно, операция записи не может быть выполнена, пока количество занятых ячеек равно 100 (или количество свободных ячеек равно 0), и операция чтения не может быть выполнена, пока количество свободных ячеек равно 100 (или количество занятых ячеек равно 0).

Рис. 3.8. Схема потоков для реализации задачи о кольцевом буфере

Реализуем задачу, используя библиотеку Pthread.

#include <pthread.h> #include <semaphore.h> #include <stdlib.h> #include <stdio.h>

int buf[100] ; //буфер

int front = 0 ; //индекс для чтения из буфера int rear = 0 ; //индекс для записи в буфер

sem_t empty ; //семафор, отображающий насколько буфер пуст sem_t full ; //семафор, отображающий насколько полон буфер

pthread_mutex_t mutexD ; //мьютекс для операции записи pthread_mutex_t mutexF ; //мьютекс для операции чтения

Средства разработки параллельных программм

83

//стартовая функция потоков – производителей(писателей) void *Producer(void *param)

{

int data, i ;

while (1)

{

//создать элемент для буфера for (i=0,data=0 ; i<100 ; i++)

data += rand()/(RAND_MAX/10) – 5 ;

//поместить элемент в буфер pthread_mutex_lock(&mutexD) ; //защита операции записи

sem_wait(&empty) ; //количество свободных ячеек уменьшить на единицу buf[rear] = data ; rear = (rear+1)%100 ; //критическая секция sem_post(&full) ; //количество занятых ячеек увеличилось на единицу pthread_mutex_unlock(&mutexD) ;

}

}

//стартовая функция потоков – потребителей(читателей) void *Consumer(void *param)

{int result ;

while (1)

{

//извлечь элемент из буфера pthread_mutex_lock(&mutexF) ; //защита операции чтения

sem_wait(&full) ; //количество занятых ячеек уменьшить на единицу result = buf[front] ; front = (front+1)%100 ; //критическая секция sem_post(&empty) ; //количество свободных ячеек увеличилось на единицу pthread_mutex_unlock(&mutexF) ;

//обработать полученный элемент fprintf(stdout,” - %d -”,result) ;

}

}

int main()

{int i ;

//инициализация мьютексов и семафоров pthread_mutex_init(&mutexD, NULL) ; pthread_mutex_init(&mutexF, NULL) ;

sem_init(&empty, 0, 100) ; //количество свободных ячеек равно 100 sem_init(&full, 0, 0) ; //количество занятых ячеек равно 0

//запуск производителей pthread_t threadP[3] ;

for (i=0 ; i<3 ; i++) pthread_create(&threadP[i],NULL,Producer,NULL) ;

//запуск потребителей pthread_t threadC[4] ; for (i=0 ; i<4 ; i++)

pthread_create(&threadC[i],NULL,Consumer,NULL) ;

Средства разработки параллельных программм

84

//пусть главный поток будет потребителем

Consumer(NULL) ;

}

Условные переменные

Условные переменные, как и семафоры, осуществляют блокировку потоков и их пробуждение. Но для семафоров четко определено условие, по которому поток останавливается и возвращается к работе, тогда как условные переменные не содержат никаких условий для управления потоками. Используя условную переменную, можно приостановить выполнение потока и ожидать пробуждения, можно «разбудить» один из потоков или все потоки, ожидающие сигнала от данной условной переменной. Все эти действия осуществляются посредством вызова функций по обработке условной переменной, при этом условная переменная не обладает никаким внутренним условием, с которым необходимо считаться при выполнении соответственных функций (подобно открыто или закрыто для мьютекса, или целого числа для семафора).

Вглаве 2 условные переменные были описаны в связи с мониторами. Основное отличие реализации условных переменных в библиотеке PTHREAD от рассмотренного ранее состоит в следующем. Использование условных переменных в мониторе предполагает неявное соблюдение взаимного исключения, поскольку в каждый момент времени процедуру монитора может выполнять только один поток. При использовании условных переменных в библиотеке PTHREAD о взаимном исключении следует заботиться дополнительно. Это происходит путем совместного использования условной переменной и мьютекса для обеспечения взаимного исключения.

Отсутствие условия на первый взгляд делает управление потоками более простым. Но эта особенность условных переменных обуславливает их ненадежность, так как поток может быть разбужен, только если он приостановлен (спит) и ожидает сигнала от условной переменной. Таким образом, если сигнал о пробуждении был сформирован до усыпления потока, то этот сигнал пропадет, поток войдет в состояние сна и далее из него не выйдет.

Всхеме, представленной на рис. 3.9. предполагается, что участок кода 4, может быть выполнен только после выполнения программного кода 1. Однако, если первый поток успеет выполниться до запуска второго потока, то сигнал о пробуждении не достигнет второго потока, поскольку второй поток не находился в состоянии сна во время прохождения этого сигнала. Таким образом, второй поток никогда не проснется и программа не завершится.

pthread_cond_t. Рассмот-

Средства разработки параллельных программм

85

Рис. 3.9. Схема неверного использования условной переменной

Так как условные переменные не описывают условий для управления потоками, эта задача перекладывается на программиста. Очень часто условия включают в себя общие данные, и эти данные необходимо защищать от одновременного использования разными потоками. Мьютекс – простое и эффективное средство защиты, и использование условных переменных предполагает и использование мьютексов, защищающих общие данные. Мьютексы также необходимы для получения корректного условия. В силу того, что условие изменяется, поток может получить устаревшее условие и заснуть (или проснуться) когда это совершенно не нужно. Структура условной переменной не включает в себя мьютекс, он должен быть описан отдельно. При этом как несколько условных переменных могут пользоваться одним мьютексом, так и одна условная переменная может использовать несколько мьютексов. С одной стороны, такая ситуация позволяет описывать более сложные алгоритмы управления потоками, но с другой стороны, это может привести к нарушению логики программы или бесконечному ожиданию открытия мьютекса или сигнала от условной переменной. Наиболее надежным является вариант, когда каждой условной переменной соответствует свой мьютекс, не используемый для других целей. Несколько условных переменных должны использовать один мьютекс, если все эти переменные опираются на проверку условий, зависящих от одних и тех же общих данных.

Необходимо заметить, что функции условных переменных можно вызывать и без описания и проверки условий (схема на рис. 3.8 не содержит условий). Но такая работа с условными переменными чаще всего не имеет смысла, из-за того, что сигналы от условной переменной пропадают, если их не ожидают в момент их подачи.

Условная переменная определяется структурой рим функции для работы с условными переменными.

int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *cond_attr);

Функция инициализирует условную переменную cond, присваивая ей атрибут cond_attr. Для инициализации условной переменной также можно использовать вызов

Средства разработки параллельных программм

86

pthread_cond_t переменная = PTHREAD_COND_INITIALIZER ;

int pthread_cond_destroy (pthread_cond_t *cond);

Функция разрушает условную переменную cond.

int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);

Поток, вызвавший эту функцию, блокируется (засыпает), при этом мьютекс mutex открывается. Когда поток будет разбужен, мьютекс снова закроется. Предполагается, что перед вызовом этой функции мьютекс должен быть закрыт. О потоке, вызвавшем функцию pthread_cond_wait(cond) говорят, что он ожидает условную переменную cond.

int pthread_cond_signal (pthread_cond_t *cond);

Функция пробуждает один поток, ожидающий условную переменную

cond.

int pthread_cond_broadcast (pthread_cond_t *cond);

Функция пробуждает все потоки, ожидающие условную переменную

cond.

Рассмотрим функции для атрибута условной переменной.

int pthread_condattr_init (pthread_condattr_t *attr);

Функция инициализирует атрибут условной переменной attr.

int pthread_condattr_destroy (pthread_condattr_t *attr);

Функция разрушает атрибут условной переменной attr.

int pthread_condattr_setpshared (pthread_condattr_t *attr, int pshared);

Функция присваивает атрибуту attr значение pshared. Если

pshared=PTHREAD_PROCESS_SHARED, условная переменная может быть ис-

пользована в различных процессах. Если pshared=PTHREAD_PROCESS_PRIVATE, условная переменная создается для использования внутри одного процесса. Необходимо заметить, что для защиты условной переменной следует использовать мьютекс того же типа, как и сама условная переменная.

int pthread_condattr_getpshared (pthread_condattr_t *attr, int *pshared);

Функция записывает значение свойства pshared атрибута attr в *pshared.

Приведенные функции возвращают ноль в случае успеха, и ненулевое значение в случае неудачи.

Из списка функций видно, что pthread_cond_wait() – функция ожидания, использует мьютекс, тогда как функции пробуждения ожидающих потоков этого не делают. При вызове этих функций мьютексы, как правило, не нужны, но если вызову функции предшествует изменение или проверка условия, то такой участок лучше защитить мьютексом.

Средства разработки параллельных программм

87

Ожидание выполнения какого-то условия с помощью условной переменной наиболее надежно реализовывается следующим образом.

pthread_mutex_lock(&mutex) ;

//закрыть мьютекс ;

while (!<условие>)

//пока (условие не выполнено)

pthread_cond_wait(&cond,&mutex) ;

//ожидать(усл.перем-я, мьютекс) ;

...

//программный код изменяющий общие

//данные, входящие в условие ;

 

pthread_mutex_unlock(&mutex) ;

//открыть мьютекс ;

...

//программный код не использующий

//общие данные, входящие в условие ;

 

Цикл предоставляет дополнительную защиту от несоответствия проверенного и текущего состояния условия. Если предполагается, что несколько потоков могут ожидать условную переменную, и для пробуждения используется функция pthread_cond_broadcast, то замена цикла (while) на команду ветвления (if) становится небезопасной, даже если все общие данные, входящие в условие, защищены мьютексом во всех потоках.

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

Стартовая функция потока потребителя должна быть следующей.

while(1)

Бесконечный цикл

{

{

pthread_mutex_lock(&mutex) ;

закрыть мьютекс ;

while (length == 0)

пока (длина очереди равна нулю)

pthread_cond_wait(&cond1,

ожидать(усл.перем-я 1, мьютекс) ;

&mutex) ;

изъять элемент из очереди ;

забрать(очередь) ;

уменьшить длину очереди на единицу;

length-- ;

открыть мьютекс ;

pthread_mutex_unlock(&mutex) ;

разбудить все потоки(усл.перем-я 2) ;

pthread_cond_broadcast(&cond2) ;

обработка полученного из очереди

...

элемента ;

}

}

 

Средства разработки параллельных программм

88

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

while(1)

Бесконечный цикл

{

{

...

сформировать элемент для очереди ;

pthread_mutex_lock(&mutex) ;

закрыть мьютекс ;

while (length > 60)

пока (длина очереди > 60)

pthread_cond_wait(&cond2, &mutex) ;

ожидать(усл.перем-я 2, мьютекс) ;

вставить(очередь) ;

добавить элемент в очередь ;

length++ ;

увеличить длину очереди на единицу ;

pthread_mutex_unlock(&mutex) ;

открыть мьютекс ;

pthread_cond_signal(&cond1) ;

разбудить один поток(усл.перем-я 1) ;

}

}

В описанном примере используется один мьютекс и две условные переменные (cond1, cond2). Переменная cond1 введена для засыпания (пробуждения) потока потребителя, cond2 для потоков производителей. Поток потребитель засыпает, если длина очереди равна нулю, и пробуждает все потоки производители (broadcast) после каждого изъятия элемента из очереди. Каждый поток производитель засыпает, если длина очереди больше шестидесяти, и пробуждает поток потребитель, после каждого добавления элемента в очередь. Все потоки пользуются одним мьютексом, это связано с тем, что условия ожидания всех потоков (length == 0, length > 60) содержат одни и те же общие данные – глобальную переменную length. Обратите внимание на то, что мьютекс защищает также процесс изменения очереди и ее длины. Таким образом, параллельное выполнение потоков возможно только во время формирования и обработки элемента очереди, добавление и изъятие элементов из очереди и проверка длины очереди может происходить только последовательно, сколько бы потоков одновременно ни выполнялось.

БЛОКИРОВКИ

Блокировки – механизмы синхронизации, работающие по принципам, схожими с двоичными семафорами. Блокировки чтения-записи могут быть несколько раз закрыты для чтения, или один раз закрыты для записи, это позволяет потокам-читателям работать совместно, или потоку-писателю изменять данные без вмешательства других потоков. Спин-блокировки могут находиться только в закрытом или открытом состоянии, их отличительная черта - активное ожидание потоком открытия спин-блокировки, другими словами, поток, заблокированный спин-блокировкой, не засыпает, а систематически опрашивает систему о состоянии спин-блокировки.

Средства разработки параллельных программм

89

Блокировки чтения-записи

Блокировки чтения-записи предоставляют возможность читать общие данные, сразу многим потокам, или изменять общие данные только одному потоку. При этом операция чтения и операция записи не могут быть выполнены одновременно, здесь под операцией понимается участок кода, защищенный соответственным замыканием блокировки.

Блокировки чтения-записи во многом похожи на двоичные семафоры. Блокировка, также как и мьютекс, может находиться или в открытом или в закрытом состоянии. Различие состоит в том, что блокировка может быть закрыта для записи или закрыта для чтения. Если блокировка закрыта для записи, то никакой другой поток не может закрыть блокировку. Если блокировка закрыта для чтения, то никакой поток не может закрыть блокировку для записи.

Если закрытие блокировки в данный момент не возможно, поток останавливается до того момента, когда блокировка станет открытой. Блокировка может быть открыта любым потоком, а не только тем, который ее закрыл.

Необходимо заметить, что само закрытие для записи или закрытие для чтения осуществляются вызовом соответственных функций блокировок чте- ния-записи, и эти функции никак не ограничивают дальнейших действий программиста. Это означает, что после закрытия блокировки на чтение можно начать изменять общие данные. Однако подобных ситуаций следует избегать, т.к. они могут привести к конфликтам. Если поток закрывает блокировку для чтения, то он не должен изменять общие данные потоков.

С помощью блокировок чтения-записи можно защищать файлы, глобальные переменные и другие общие ресурсы.

Блокировки чтения-записи описываются структурой pthread_rwlock_t. Рассмотрим функции для работы с блокировками чтения-записи.

int pthread_rwlock_init (pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

Функция инициализирует блокировку чтения-записи rwlock, присваивая ей атрибут attr. Блокировка создается в открытом состоянии.

int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);

Функция разрушает блокировку чтения-записи rwlock.

int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);

Функция закрывает блокировку rwlock для чтения. Если при вызове функции, блокировка закрыта для записи, выполнение потока, вызвавшего данную функцию, приостанавливается до тех пор, пока блокировка не откроется. Если при вызове функции, блокировка закрыта для чтения или открыта, то функция выполняется и поток продолжает работу. Таким образом, блокировка может быть закрыта для чтения несколько раз, чтобы такая блокировка

Средства разработки параллельных программм

90

стала открытой, необходимо открыть ее такое же количество раз, сколько она была закрыта.

int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);

Функция закрывает блокировку rwlock для чтения, если она открыта или закрыта для чтения. Если при вызове функции, блокировка закрыта для записи, то функция возвратит ненулевое значение. Функция tryrdlock осуществляет закрытие блокировки для чтения, не блокируя выполнение потока, ее вызвавшего.

int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);

Функция закрывает блокировку rwlock для записи. Если при вызове функции, блокировка закрыта, выполнение потока, вызвавшего данную функцию, приостанавливается до тех пор, пока блокировка не откроется. Если при вызове функции, блокировка открыта, то функция выполняется и поток продолжает работу. Таким образом, блокировка не может быть закрыта для записи несколько раз.

int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);

Функция закрывает блокировку rwlock для записи, если она открыта. Если при вызове функции, блокировка закрыта, то функция возвратит ненулевое значение. Функция trywrlock осуществляет закрытие блокировки для записи, не блокируя выполнение потока, ее вызвавшего.

int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);

Функция открывает блокировку rwlock. Блокировку, закрытую для чтения, открывают, вызывая функцию unlock столько раз, сколько эта блокировка была закрыта. Для открытия блокировки, закрытой для записи, достаточно одного вызова функции unlock. Открытие уже открытой блокировки не приведет к остановке потока.

Рис. 3.10. Упрощенная схема функций rdlock(), wrlock()

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