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

u_course

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

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

91

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 3.11. Схема функций tryrdlock(), trywrlock()

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

int pthread_rwlockattr_init (pthread_rwlockattr_t *attr);

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

int pthread_rwlockattr_destroy (pthread_rwlockattr_t *attr);

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

int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *attr, int pshared);

Функция устанавливает свойство pshared для атрибута attr. Если

pshared=PTHREAD_PROCESS_PRIVATE, блокировка может быть использована

только в одном процессе. Если pshared=PTHREAD_PROCESS_SHARED, блокировка может быть использована во многих процессах.

int pthread_rwlockattr_setkind_np (pthread_rwlockattr_t *attr, int pref);

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

PTHREAD_RWLOCK_PREFER_READER_NP – при наличии нескольких запросов на

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

PTHREAD_RWLOCK_PREFER_WRITER_NP – принят по умолчанию, в первую оче-

редь будут удовлетворены запросы закрытия для записи.

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

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

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

pthread_spinlock_t

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

92

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

Спин-блокировки

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

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

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

int pthread_spin_init (pthread_spinlock_t *spinlock, int pshared);

Функция инициализирует спин-блокировку spinlock, если pshared больше нуля, блокировка может быть использована во многих процессах, если pshared

pthread_barrier_t, рассмотрим функции

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

93

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

int pthread_spin_destroy (pthread_spinlock_t *spinlock);

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

int pthread_spin_lock (pthread_spinlock_t *spinlock);

Функция закрывает спин-блокировку spinlock. Если блокировка уже закрыта, то поток, вызвавший эту функцию, переходит в режим активного ожидания открытия блокировки. И когда блокировка станет открытой, функция ее закроет и поток продолжит выполнение.

int pthread_spin_trylock (pthread_spinlock_t *spinlock);

Функция закрывает спин-блокировку spinlock, если та является открытой. Если же блокировка закрыта, функция trylock возвратит ненулевое значение и поток продолжит выполнение.

int pthread_spin_unlock (pthread_spinlock_t *spinlock);

Функция открывает спин-блокировку spinlock.

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

БАРЬЕРЫ

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

Барьер описывается структурой для работы с барьером.

int pthread_barrier_init (pthread_barrier_t *barrier, pthread_barrierattr_t *attr,

unsigned int count);

Функция инициализирует барьер barrier, присваивая ему атрибут attr и целое положительное число count.

int pthread_barrier_destroy (pthread_barrier_t *barrier);

Функция разрушает барьер barrier.

int pthread_barrier_wait (pthread_barrier_t *barrier);

Функция блокирует вызвавший ее поток, до того момента, пока эта функция не будет вызвана то количество раз, которое было указано при инициализации барьера (count). Поскольку функция блокирует поток, то фактически количество вызовов этой функции для одного барьера, всегда равно

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

94

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

Рассмотрим функции, применяемые к атрибуту барьера.

int pthread_barrierattr_init (pthread_barrierattr_t *attr);

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

int pthread_barrierattr_destroy (pthread_barrierattr_t *attr);

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

int pthread_barrierattr_setpshared (pthread_barrierattr_t *attr, int pshared);

Функция устанавливает свойство pshared для атрибута attr. Если pshared равно PTHREAD_PROCESS_PRIVATE, барьер может быть использован только в одном процессе. Если pshared равно PTHREAD_PROCESS_SHARED, барьер может быть использован во многих процессах.

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

#include <pthread.h> pthread_barrier_t barr; //барьер

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

{

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

pthread_barrier_wait(&barr); //установить барьер

}

int main()

{

pthread_barrier_init(&barr, NULL, 7); //создание барьера со значением 7 pthread_t thread[6] ; //идентификаторы для шести потоков

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

pthread_barrier_wait(&barr); //установить барьер

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

}

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

95

Обратите внимание, что в примере барьеру приписывается значение «семь», поскольку главный поток тоже должен входить в группу потоков, блокируемых барьером.

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

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

#include <pthread.h>

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

int Max[2] ; //массив, содержащий максимумы, вычисленные потоками

int MaxPar[2][2] ; //массив, содержащий выделенную пару элементов, для каждого потока

pthread_barrier_t barr ; //барьер

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

{

int begin,end ; //границы рассмотрения массива int max,i ; //максимум и счетчик для цикла

if ((int)param==0)

{ begin=0 ; end=100 ; }

//границы для первого потока

if ((int)param==1)

{ begin=100 ; end=200 ; }

//границы для второго потока

//внесем в результирующие массивы, начальные значения (свои для каждого потока) // из исходного массива

Max[(int)param] = Mas[begin]*Mas[begin+1] ; MaxPar[(int)param][0] = Mas[begin] ; MaxPar[(int)param][1] = Mas[begin+1] ;

//найдем пару элементов, удовлетворяющих условию задачи //каждый поток работает со своей частью массива

for (i=begin; i<end-1 ; i++)

{

max=Mas[i]*Mas[i+1] ;

if (max > Max[(int)param])

{

Max[(int)param] = max ; MaxPar[(int)param][0] = Mas[i] ; MaxPar[(int)param][1] = Mas[i+1] ;

}

}

//остановим поток, пока другой поток не дойдет до этой же точки выполнения pthread_barrier_wait(&barr) ;

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

96

}

int main()

{

pthread_barrier_init(&barr, NULL, 2) ; //инициализация барьера pthread_t mythread ;

pthread_create(&mythread, NULL, func, (void *)0) ; //создание дочернего потока func((void *)1) ; //главный поток обрабатывает свою часть массива

//проверка граничных элементов разбиений массива max=Mas[99]*Mas[100] ;

if (max > Max[1])

{Max[1] = max ; MaxPar[1][0] = Mas[99] ; MaxPar[1][1] = Mas[100] ;}

//определение максимального значения из значений, найденных обоими потоками int i ;

if (Max[0]>Max[1]) i=0 ; else i=1 ;

fprintf(stdout,"\nМаксимальное произведение %d*%d = %d", MaxPar[i][0], MaxPar[i][1], Max[i]) ; //вывод результата

return 1;

}

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

В описанном примере (рис. 3.13), использование барьера позволяет выполнить проверку граничных элементов и вывести окончательный результат, только после того, как оба потока обработают свои части исходного массива.

КОНТРОЛЬНЫЕВОПРОСЫ

1.Для чего предназначена библиотека Pthread?

2.Общая структура многопоточной программы, написанной с использованием библиотеки Pthread.

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

97

3.Как компилируется приложение, использующее библиотеку Pthread?

4.Как синхронизируются потоки в библиотеке Pthread?

5.Назовите основные синхропримитивы, реализованные в библиотеке

Pthread.

6.Опишите формат функции, обеспечивающий создание потока в биб-

лиотеке Pthread.

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

Pthread?

8.Для чего используются атрибуты потоков? Какие функции используются для работы с ними?

9.Что такое мьютекс? Как мьютексы используются для синхронизации потоков?

10.Какие функции используются для работы мьютексов?

11.Реализация задачи критической секции с помощью мьютексов.

12.Особенности реализации семафоров в библиотеке Pthread.

13.Чем мьютекс отличается от семафора в библиотеке Pthread?

14.Опишите решение задачи о кольцевом буфере.

15.Каким образом в библиотеке Pthread используются условные пере-

менные?

16.Основные виды блокировок, применяемых в библиотеке Pthread.

17.В чем отличие и в чем сходство мьютексов и блокировок?

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

Pthread.

19.Особенности блокировок чтения-записи.

20.Особенности спин-блокировок.

21.Особенности организации и использования барьеров в библиотеке

Pthread.

22.Основные функции для работы с барьерами в библиотеке Pthread.

ГЛАВА4. УПРАВЛЕНИЕПОТОКАМИСПОМОЩЬЮ ФУНКЦИЙWINAPI

ОБЪЕКТЫЯДРА

Основные понятия

Операционная система Windows позволяет оперировать несколькими типами объектов ядра, такими как процессы, потоки, проекции файлов, события, семафоры, мъютексы, каналы и т.д.

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

Для того чтобы создать объект ядра, необходимо в программе вызвать функцию Win32 API. Например, CreateFileMapping() заставляет систему сформировать объект «проекция файла» (file-mapping object).

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

Каким же образом приложение может оперировать объектами ядра? Win32 API предусматривает набор функций, обрабатывающих объекты ядра по строго определенным правилам. Доступ к объектам ядра может быть получен только через эти функции. Когда вызывают функцию, создающую объект ядра, она возвращает описатель, идентифицирующий созданный объект. Описатель следует рассматривать как «непрозрачное» 32-битное значение (в программах C это переменная типа HANDLE), которое может быть использовано любым потоком процесса. Приложение передает описатель объекта ядра Win32-функциям, сообщая системе, какой объект ядра его интересует.

Для большей надежности ОС значения описателей зависят от кон-

кретного процесса. Даже если с помощью какого-либо механизма межпроцессной связи описатель объекта, созданного в процессе «1» передать потоку процесса «2», то любой вызов из процесса «2» со значением полученного описателя даст ошибку.

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

99

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

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

Таблица описателей

При инициализации процесса система создает в нем таблицу описателей, используемую только для объектов ядра. Сведения о структуре этой таблицы и управлении незадокументированы. На рис. 4.1 представлена примерная структура таблицы описателей.

Индекс

Указатель на блок памяти объекта

Маска дос-

Флаги

 

ядра

тупа

 

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

Когда процесс инициализируется в первый раз, таблица описателей еще пуста. При вызове одним из потоков процесса Win32 API функции, создающей объект ядра (например, CreateFileMapping()), ядро выделяет для этого объекта блок памяти и инициализирует его; далее ядро просматривает таблицу описателей, принадлежащую данному процессу, и отыскивает свободную запись. Поскольку таблица еще пуста, ядро обнаруживает структуру с индексом 1 и инициализирует ее. Указатель устанавливается на внутренний адрес памяти структуры данных объекта ядра, маска доступа – на доступ без ограничений, и, наконец, определяется последний компонент – флаги.

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

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

100

Независимо от того, как именно создан объект ядра, по окончании работы с ним его нужно закрыть вызовом Win32 API функции CloseHandle():

BOOL CloseHandle(HANDLE hobj);

Эта функция сначала проверяет таблицу описателей, принадлежащую вызывающему процессу, чтобы убедиться, идентифицирует ли переданный ей индекс (описатель) объект, к которому этот процесс действительно имеет доступ. Если переданный описатель неверен, функция возвращает FALSE, a

функция GetLastError() – код ERROR_INVALID_HANDLE. Если же индекс достове-

рен, система получает адрес структуры данных объекта ядра и уменьшает в этой структуре счетчик числа пользователей. Как только счетчик обнулится, ядро удалит объект из памяти. Перед самым возвратом управления CloseHandle() удаляет соответствующую запись из таблицы описателей, после этого данный описатель недействителен в вызвавшем закрытие процессе, использовать его нельзя. Запись из таблицы описателей процесса удаляется независимо от того, разрушен объект ядра или нет. После вызова CloseHandle() процесс больше не получит доступ к этому объекту ядра.

Совместное использование объектов ядра несколькими процессами

Существует необходимость в совместном использовании объектов ядра потоками, исполняемыми в разных процессах. Например:

объекты «проекции файлов» (file-mapping object) позволяют двум процессам, исполняемым на одной машине, совместно использовать одни и те же блоки данных;

«почтовые ящики» (mailslots) и именованные каналы (named pipes) дают возможность обмениваться данными процессам, исполняемым на разных машинах в сети;

мьютексы (mutexes), семафоры (semaphores) и события (events) по-

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

Но описатели объектов ядра имеют смысл только в конкретном процессе, поэтому разделение объектов ядра между несколькими процессами в Win32 – задача весьма непростая.

У разработчиков ОС было несколько веских причин сделать описатели «процессо-зависимыми», основными считаются – устойчивость операционной системы к сбоям и защита. Объекты ядра защищены, процесс, прежде чем оперировать с объектом, должен запросить разрешение на доступ к нему

уОС. Процесс-создатель объекта может предотвратить несанкционированный доступ к этому объекту со стороны другого процесса.

Существует три способа разделения объектов ядра.

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