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

u_course

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

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

71

pthread_attr_init(&attr) ; // инициализация переменной атрибута

//укажем для свойства detachstate значение PTHREAD_CREATE_DETACHED pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) ;

//укажем для свойства inherit значение PTHREAD_EXPLICIT_SCHED

//это необходимо, для того, чтобы поток получил информацию о приоритете

//из переменной атрибута

pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED) ; sched_param param ; //переменная параметра планирования param.sched_priority = 0 ; //установим приоритет в 0

//внесем значение параметра планирования в переменную атрибута attr pthread_attr_setschedparam(&attr,&param) ;

//создадим два потока с одинаковыми атрибутами

//одна и та же переменная атрибута может быть использована

//для нескольких потоков

pthread_create(&mythread1, &attr, func, NULL) ; pthread_create(&mythread2, &attr, func, NULL) ;

// запишем значение свойства detachstate переменной атрибута attr в i int i ; pthread_attr_getdetachstate(&attr,&i) ;

СИНХРОНИЗАЦИЯПОТОКОВСПОМОЩЬЮСЕМАФОРОВ

Двоичные семафоры

При обращении нескольких потоков к общим ресурсам, могут возникать конфликтные ситуации. Например, если несколько потоков осуществляют запись данных в один и тот же файл, или при совместном использовании глобальных переменных. В последнем случае ошибки чаще возникают из-за неверной логики построения взаимодействия потоков. Для иллюстрации рассмотрим следующий пример, пусть несколько потоков периодически обращаются к глобальной переменной, и если она отлична от нуля, используют ее для деления, кроме того, потоки изменяют значение этой переменной (рисунок 3.2). Ситуация может стать критической, если поток А вычислил условие (!=0), сразу после этого сработал поток Б и изменил значение на ноль, после этого управление перешло к потоку А, и он произвел деление на ноль. Более подробно ситуации, связанные с тем, что поток может обладать некоторым участком кода (критической секцией), который должен быть выполнен без передачи управления какому-то другому потоку, описаны в главе 2.

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

72

Рис. 3.2. Схема потоков

Вбиблиотеке PTHREAD для предотвращения ошибок взаимодействия

иконфликтов, можно воспользоваться двоичными или обычными семафорами. Двоичные семафоры (мьютексы) могут находиться в двух состояниях: открытом (bsem=1) и закрытом (bsem=0). Поток может закрыть только открытый мьютекс. Если поток пытается закрыть уже закрытый мьютекс, то выполнение этого потока приостанавливается, до тех пор, пока мьютекс не станет открытым:

P(bsem): <await (bsem>0) bsem=bsem-1;>.

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

V(bsem): <if (bsem==0) bsem=bsem+1; else bsem=bsern>.

Мьютекс создается в открытом состоянии (bsem=1).

Cледует отметить, что мьютекс может быть “рекурсивным” (PTHREAD_MUTEX_RECURSIVE). В этом случае он может быть закрыт одним потоком несколько раз. Для того чтобы закрытый несколько раз потоком рекурсивный мьютекс перешел в открытое состояние, требуется открыть его такое же количество раз в этом же потоке, сколько он был закрыт. Рекурсивный мутекс подобен обычному семафору, отличие состоит в том, что состояние семафора может быть изменено различными потоками.

Двоичный семафор описывается структурой pthread_mutex_t. Ниже рассматриваются функции, используемые для работы с ним.

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

Функция инициализирует переменную мьютекса mutex, ассоциируя с ней атрибут мьютекса attr. Если атрибут не определен пользователем, инициализированный мьютекс является обычным, нерекурсивным. Альтернативой вызова функции pthread_mutex_init(), без задания атрибута, является при-

своение мьютексу константы PTHREAD_MUTEX_INITIALIZER.

PTHREAD_MUTEX_NORMAL

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

73

int pthread_mutex_destroy (pthread_mutex_t *mutex);

Функция разрушает переменную мьютекса mutex.

int pthread_mutex_lock (pthread_mutex_t *mutex);

Функция закрывает мьютекс mutex, если тот уже закрыт, то ждет, пока мьютекс не станет открытым, и тогда закрывает его. Следует понимать, что поток не должен закрывать уже закрытый им мьютекс (если он не является рекурсивным), так как это может привезти к бесконечному ожиданию открытия мьютекса.

int pthread_mutex_unlock (pthread_mutex_t *mutex);

Функция открывает мьютекс mutex. Если мьютекс уже является открытым, это не приведет к ожиданию закрытия мьютекса.

int pthread_mutex_trylock (pthread_mutex_t *mutex);

Функция пытается закрыть мьютекс mutex, если мьютекс уже закрыт, поток, вызвавший функцию trylock, продолжит выполнение, он не станет ожидать открытия мьютекса.

Рассмотрим функции для работы с атрибутом двоичного семафора

pthread_mutexattr_t.

int pthread_mutexattr_init (pthread_mutexattr_t *attr);

Функция инициализирует атрибут мьютекса attr.

int pthread_mutexattr_destroy (pthread_mutexattr_t *attr);

Функция разрушает атрибут мьютекса attr.

int pthread_mutexattr_settype (pthread_mutexattr_t *attr, int kind);

Функция ассоциирует с атрибутом attr, свойство типа мьютекса kind. Свойство kind может принимать одно из следующих значений.

PTHREAD_MUTEX_RECURSIVE описывает рекурсивный мьютекс. PTHREAD_MUTEX_ERRORCHECK описывает нерекурсивный мьютекс, который

может быть открыт только тем потоком, который его закрыл.

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

PTHREAD_MUTEX_NORMAL задает обычный, нерекурсивный мьютекс. Такой мьютекс может быть открыт любым потоком.

PTHREAD_MUTEX_DEFAULT тип мьютекса по умолчанию, как правило, это аналогично типу .

int pthread_mutexattr_gettype (const pthread_mutexattr_t *attr, int *kind);

Функция записывает значение свойства kind атрибута мьютекса attr в

*kind.

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

74

Рассмотрим примеры использования мьютекса.

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

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

Главный поток

//инициализация глобальной переменной mutex-структуры pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;

Первый поток-потомок

Второй поток-потомок

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

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

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

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

pthread_mutex_lock(&mutex) ;

pthread_mutex_lock(&mutex) ;

//Критический участок

//Критический участок

Запись в файл

Запись в файл

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

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

pthread_mutex_unlock(&mutex) ;

pthread_mutex_unlock(&mutex) ;

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

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

 

 

Рис. 3.3. Схема, демонстрирующая использование мьютекса

Рассмотрим простой пример некорректного использования мьютекса.

pthread_mutex_t mutex ; //переменная мьютекса pthread_mutexattr_t mattr ; //переменная атрибута мьютекса

pthread_mutexattr_init(&mattr) ; //инициализация переменной атрибута мьютекса

//установим тип мьютекса в PTHREAD_MUTEX_NORMAL pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_NORMAL) ;

pthread_mutex_init (&mutex, &mattr) ; //инициализация мьютекса

pthread_mutex_lock(&mutex) ; //закрытие мьютекса 1 pthread_mutex_lock(&mutex) ; //закрытие мьютекса 2

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

pthread_mutex_unlock(&mutex) ; //открытие мьютекса 1 pthread_mutex_unlock(&mutex) ; //открытие мьютекса 2

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

75

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

Если установить тип мьютекса в PTHREAD_MUTEX_ERRORCHECK, то наличие другого потока, открывающего мьютекс, не спасет положение, в силу того, что такой мьютекс может быть открыт только тем потоком, который его закрыл. Если установить тип мьютекса в PTHREAD_MUTEX_RECURSIVE, то поток вовсе не придет к бесконечному ожиданию, в таком случае код корректен, мьютекс был закрыт дважды, и затем дважды открыт.

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

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

int Source[1000] ; //исходный массив

int Out[1000] ; //результирующий массив

int Outcount=0 ; //количество элементов в результирующем массиве

//структура для передачи потокам границ рассмотрения исходного массива struct bound

{

int begin ; int end ;

} ;

pthread_mutex_t mutex; //мьютекс

//стартовая функция потоков void * func (void *param)

{

int i ; float s ;

bound *b = (bound *)param ;

for (i=b->begin ; i<b->end ; i++)

{

s = sqrt(Source[i]) ;

//если элемент массива является квадратом целого числа if (s == floor(s) )

{

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

76

//закрыть мьютекс, перед записью значения в результирующий массив pthread_mutex_lock(&mutex) ;

Out[Outcount] = Source[i] ; //критический участок

Outcount++ ; //критический участок

//открыть мьютекс pthread_mutex_unlock(&mutex) ;

}

}

return NULL ;

}

int main()

{

int i ;

for (i=0 ; i<1000 ; i++)

{//заполнение массива числами от 1 до 4000

Source[i] = 1 + (int)(random()/(RAND_MAX/3999)) ;

}

pthread_mutexattr_t mattr ; //переменная атрибута мьютекса pthread_mutexattr_init(&mattr) ; //инициализация переменной атрибута мьютекса pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_ERRORCHECK) ;

pthread_mutex_init (&mutex, &mattr) ; //инициализация мьютекса

bound b,c ;

pthread_t thread1, thread2 ;

//определим границы для первого потока b.begin = 0 ; b.end = 500 ; pthread_create(&thread1,NULL,func,(void *)&b) ;

//определим границы для второго потока c.begin = 500 ; c.end = 1000 ; pthread_create(&thread2,NULL,func,(void *)&c) ;

//дождемся завершения работы созданных потоков pthread_join(thread1,NULL) ; pthread_join(thread2,NULL) ;

//выведем результат for (i=0 ; i<Outcount ; i++)

fprintf(stdout,"// %d ",Out[i]) ;

return 1 ;

}

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

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

77

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 3.4. Схема потоков

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

Семафоры

Каждый семафор содержит неотрицательное целое значение. Любой поток может изменять значение семафора. Уменьшение значения семафора, происходит следующим неделимым образом:

P(sem): <await (sem>0) sem=sem-1;>

если значение семафора больше нуля, то оно уменьшается, если же значение равно нулю, поток приостанавливается до того момента, когда значение семафора станет положительным1, тогда значение уменьшается и поток продолжает работу. Увеличение значения семафора, возможно всегда:

V(sem): <sem=sem+1;>

Эта операция не предполагает блокировки. Однако значение семафора не должно выходить за границы типа unsigned int.

Для описания семафора используется структура sem_t. Рассмотрим функции для работы с семафорами.

1 Следует помнить, что в этом случае положительным значение семафора может стать только в результате работы другого потока.

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

78

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

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

int sem_destroy (sem_t *sem);

Функция разрушает семафор sem и возвращает ноль в случае успеха.

int sem_getvalue (sem_t *sem, int *sval);

Функция записывает значение семафора sem в *sval. Эта функция не является неделимой, другими словами, если несколько потоков используют семафор, полученное значение семафора может быть устаревшим.

int sem_post (sem_t *sem);

Функция увеличивает значение семафора sem на единицу.

int sem_wait (sem_t *sem);

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

int sem_trywait (sem_t *sem);

Если текущее значение семафора больше нуля, функция уменьшает значение семафора sem на единицу и возвращает ноль. Если текущее значение семафора равно нулю, функция возвращает не нулевое значение. Функция sem_trywait не останавливает работу потока.

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

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

(рис. 3.5).

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

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

79

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

Рис. 3.5. Блокировка с помощью семафора

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

#include <pthread.h>

#include <semaphore.h> sem_t sem ; //семафор

//стартовая функция для потоков void * func(void * param)

{

... // Некоторый код

sem_post(&sem) ; // увеличение значения семафора на единицу

}

int main()

{

sem_init (&sem, 1, 0); //создание семафора со значением 0 pthread_t thread[6]; //идентификаторы для шести потоков

for (int i=0 ; i<6 ; i++) //создание шести потоков pthread_create(&thread[i], NULL, func, NULL) ;

sem_post
pthread_mutex_lock
sem_wait

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

80

for (int i=0 ; i<6 ; i++) //ожидание завершения всех потоков: общее уменьшение // значения семафора на шесть единиц

sem_wait (&sem) ; //уменьшение значения семафора на единицу

... // Некоторый код

}

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

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

для мьютексов.

Рис. 3.6. Функции sem_wait() и pthread_mutex_lock()

Функции semtrywait, sem_post и pthread_mutex_trylock, pthread_mutex_unlock

изменяют состояние семафора (или мьютекса) без блокировки потока

(рис. 3.7).

Функции sem_post и pthread_mutex_unlock после изменения состояния семафора (мьютекса) пробуждают один из потоков, ожидающих выполнения условия именно для этого семафора или мьютекса.

Семафоры и мьютексы – реентерабельные объекты, т.е. они безопасны для использования в различных потоках, несколько потоков не могут одновременно изменять или проверять их значения. Фактически, вызов любой из приведенных функций блокирует на время ее выполнения доступ к данному семафору. Функции sem_wait и pthread_mutex_lock снимают блокировку семафора перед усыплением потока, и снова устанавливают ее перед проверкой условия. Функция блокирует семафор перед увеличением его значения

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