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

u_course

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

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

131

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

HANDLE hSem;

DWORD WINAPI ThreadFunc(PVOID pvParam)

{

while(TRUE)

{

WaitForSingleObject(hSem, INFINITE); // P()

... //обработка защищенного блока данных

ReleaseSemaphore(hSem, 1, NULL); // V()

}

return 0;

}

int main(int argc, char** argv)

{

hSem = CreateSemaphore(NULL, 1, 1, “ForReadAndWrite”); … //вызов потока ThreadFunc несколько раз

CloseHandle(hSem);

return 0;

}

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

Мьютексы

Объект ядра «мьютекс» реализует двоичный семафор. Эти объекты весьма похожи на критические секции – за исключением того, что с их помощью можно синхронизировать доступ к данным со стороны нескольких процессов. Мьютексы Windows также похожи на мьютексы библиотеки Pthread, описанные в главе 3.

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

HANDLE CreateMutex(

PSECURITY_ATTRIBUTES Ipsa, BOOL fInitialOwner,

LPTSTR IpszMutexName);

Функциясоздаетобъектядра«мьютекс».

ПараметрIpsa указывает наструктуруSECURITY_ATTRIBUTES.

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

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

132

освободит его. Передача FALSE в параметре flnitialOwner подразумевает, что объект-мьютекс не принадлежит ни одному из потоков и поэтому “рождается свободным”. Первый же поток из числа ожидающих этот объектможетзанятьего, темсамымпродолжитьсвоевыполнение.

Параметр IpszMutexName содержит либо NULL, либо адрес строки (с нулевым символом в конце), идентифицирующей мьютекс. Когда приложение вызывает CreateMutex, система создает объект ядра “мьютекс” и присваивает ему имя, на которое указывает параметр IpszMutexName. Это имя используется при совместном доступе к нему нескольких процессов. CreateMutex возвращает “процессо-зависимый” описатель, определяю- щийсозданныйобъект-мьютекс.

Разумеется, любой процесс может получить свой, процессо-зависимый описатель существующего объекта «мьютекс», вызвав функцию

HANDLE OpenMutex (

DWORD fdwAccess, BOOL fInherit,

PCTSTR IpszName);

Функция открывает существующий объект ядра «мьютекс».

Параметр fdwAccess может быть равен либо SYNCHRONIZE, либо MUTEX_ALL_ACCESS.

Параметр fInherit определяет, унаследует ли дочерний процесс данный описатель данного объекта-мьютекса.

Параметр IpszName — это имя объекта-мьютекса в виде строки с нулевым символом в конце.

При вызове OpenMutex() система сканирует существующие объектымьютексы, проверяя, нет ли среди них объекта с именем, указанным в параметре IpszName. Обнаружив таковой, она создает описатель объекта, специфичный для данного процесса, и возвращает его вызвавшему потоку. В дальнейшем любой поток из данного процесса может использовать этот описатель при вызове любой функции, требующей такой описатель. Если объектамьютекса с указанным именем нет, функция возвращает NULL. Оба описанных выше способа требуют, чтобы у объекта-мьютекса было какое-то имя.

Поток занимает объект-мьютекс в результате успешного завершения ожидания одной из wait-функций (например, WaitForSingleObject()).

BOOL RelaaseMutex(HANDLE hMutex);

Функция возвращает мьютекс в свободное состояние. Параметр hMutex

– это указатель того мьютекса, который освобождается.

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

конце – ReleaseMutex().

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

133

Объект-мьютекс отличается от других синхронизирующих объектов ядра тем, что занявшему его потоку передаются права на владение им. Прочие синхронизирующие объекты могут быть либо свободны, либо заняты – и все. А объекты-мьютексы способны еще и запоминать, какому потоку они принадлежат.

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

Проблемы условной синхронизации

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

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

Проиллюстрируем поведение объектов на следующем примере. Основной поток создает объект ядра (событие, семафор, мьютекс), инициализируя его «свободным». Затем основной поток создает три потока. Первый поток занимает объект с помощью Wait-функции, второй поток пытается освободить объект, а третий ожидает его освобождения снова Wait-функцией. Для того, что бы потоки срабатывали последовательно, используется оператор Sleep(). Если сброс объекта в свободное состояние возможен, то третий поток должен получить управление, если нет – то программа зависнет.

Ниже приведены три программы, реализующие этот эксперимент. Программа, демонстрирующая работу с событиями.

#include <stdio.h> #include <conio.h> #include <windows.h>

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

134

HANDLE hEvent;// описатель событий

DWORD WINAPI First(PVOID pvParam) // поток, первым занимающий событие

{

int num;

num = *((int *)pvParam);

DWORD dw = WaitForSingleObject(hEvent,INFINITE); switch(dw) // определения способа завершения ожидания

{

case WAIT_OBJECT_0: // процесс успешно завершился printf("\n thread %d: OK",num);

break;

case WAIT_TIMEOUT: // процесс не завершился, ожидание прервано printf("\n thread %d: Timeout",num);

break;

case WAIT_FAILED: // неправильный вызов функции printf("\n thread %d: Failure",num);

break;

}

Sleep(50000); // задержка, позволяющая отработать всем потокам

return 0;

}

DWORD WINAPI Second(PVOID pvParam) // поток пытается освободить событие

{

int num; long prev;

num = *((int *)pvParam); printf("\n thread %d: Start",num);

Sleep(5000); // задержка, позволяющая первому потоку занять событие SetEvent(hEvent); // освобождение события, занятого в другом потоке printf("\n thread %d: Event free",num);

return 0;

}

DWORD WINAPI Third(PVOID pvParam)

{

int num;

num = *((int *)pvParam);

Sleep(10000); // задержка, позволяющая отработать первым двум потокам

DWORD dw = WaitForSingleObject(hEvent,INFINITE); switch(dw) // определения способа завершения ожидания

{

case WAIT_OBJECT_0: // процесс успешно завершился printf("\n thread %d: OK",num);

break;

case WAIT_TIMEOUT: // процесс не завершился, ожидание прервано printf("\n thread %d: Timeout",num);

break;

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

135

case WAIT_FAILED: // неправильный вызов функции printf("\n thread %d: Failure",num);

break;

}

return 0;

}

int main(int argc, char** argv)

{

int i, x[3];

DWORD dwThreadId[3],dw; HANDLE hThread[3];

hEvent = CreateEvent(NULL,FALSE,TRUE,"Event1"); // создание события с автосбросом

// ВНИМАНИЕ! участок кода общий для всех трех примеров

x[0] = 0;

hThread[0] = CreateThread(NULL,0,First,(PVOID)&x[0], 0, &dwThreadId[0]); if(!hThread)

printf("main process: thread %d not execute!",0);

x[1] = 1;

hThread[1] = CreateThread(NULL,0,Second,(PVOID)&x[1], 0, &dwThreadId[1]); if(!hThread)

printf("main process: thread %d not execute!",1);

x[2] = 2;

hThread[2] = CreateThread(NULL,0,Third,(PVOID)&x[2], 0, &dwThreadId[2]); if(!hThread)

printf("main process: thread %d not execute!",1);

dw = WaitForMultipleObjects(3,hThread,TRUE,INFINITE);

//конец общего участка кода, в последующих примерах помечен

//текстом «СОЗДАНИЕ И ОЖИДАНИЕ ЗАВЕРШЕНИЯ ПОТОКОВ»

//закрытие описателей событий

CloseHandle(hEvent); return 0;

}

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

#include <stdio.h> #include <conio.h> #include <windows.h>

HANDLE hSem; // описатель семафора

DWORD WINAPI First(PVOID pvParam) // поток, первым занимающий семафор

{

int num;

num = *((int *)pvParam);

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

136

DWORD dw = WaitForSingleObject(hSem,INFINITE); switch(dw) // определения способа завершения ожидания

{

case WAIT_OBJECT_0: // процесс успешно завершился printf("\n thread %d: OK",num);

break;

case WAIT_TIMEOUT: // процесс не завершился, ожидание прервано printf("\n thread %d: Timeout",num);

break;

case WAIT_FAILED: // неправильный вызов функции printf("\n thread %d: Failure",num);

break;

}

Sleep(50000); // задержка, позволяющая отработать всем потокам

return 0;

}

DWORD WINAPI Second(PVOID pvParam) // поток пытается освободить семафор

{

int num; long prev;

num = *((int *)pvParam); printf("\n thread %d: Start",num);

Sleep(5000); // задержка, позволяющая первому потоку занять событие

ReleaseSemaphore(hSem,1,&prev); // освобождение семафора, занятого в другом потоке

printf("\nthread %d: Semaphore released",num);

return 0;

}

DWORD WINAPI Third(PVOID pvParam)

{

int num;

num = *((int *)pvParam);

Sleep(10000); // задержка, позволяющая отработать первым двум потокам

DWORD dw = WaitForSingleObject(hSem,INFINITE); switch(dw) // определения способа завершения ожидания

{

case WAIT_OBJECT_0: // процесс успешно завершился printf("\n thread %d: OK",num);

break;

case WAIT_TIMEOUT: // процесс не завершился за 5000 мс, ожидание прервано printf("\n thread %d: Timeout",num);

break;

case WAIT_FAILED: // неправильный вызов функции printf("\n thread %d: Failure",num);

break;

}

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

137

return 0;

}

int main(int argc, char** argv)

{

int i, x[3];

DWORD dwThreadId[3],dw; HANDLE hThread[3];

//создание одноместного семафора с одним свободным местом hSem = CreateSemaphore(NULL,1,1,"SemaphoreStop");

//СОЗДАНИЕ И ОЖИДАНИЕ ЗАВЕРШЕНИЯ ПОТОКОВ, см предыдущий пример

//закрытие описателей событий

CloseHandle(hEvent); return 0;

}

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

#include <stdio.h> #include <conio.h> #include <windows.h>

HANDLE hMutex; // описатель мьютекса

DWORD WINAPI First(PVOID pvParam) // поток, первым занимающий мьютекс

{

int num;

num = *((int *)pvParam);

DWORD dw = WaitForSingleObject(hMutex,INFINITE); switch(dw) // определения способа завершения ожидания

{

case WAIT_OBJECT_0: // процесс успешно завершился printf("\n thread %d: OK",num);

break;

case WAIT_TIMEOUT: // процесс не завершился, ожидание прервано printf("\n thread %d: Timeout",num);

break;

case WAIT_FAILED: // неправильный вызов функции printf("\n thread %d: Failure",num);

break;

}

Sleep(50000); // задержка, позволяющая отработать всем потокам

return 0;

}

DWORD WINAPI Second(PVOID pvParam) // поток пытается освободить семафор

{

int num; long prev;

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

138

num = *((int *)pvParam); printf("\n thread %d: Start",num);

Sleep(5000); // задержка, позволяющая первому потоку занять событие

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

//и проверка результата

if(!ReleaseMutex(hMutex))

printf("\nthread %d: Error in release of mutex",num); else

printf("\nthread %d: Mutex free",num);

return 0;

}

DWORD WINAPI Third(PVOID pvParam)

{

int num;

num = *((int *)pvParam);

Sleep(10000); // задержка, позволяющая отработать первым двум потокам

DWORD dw = WaitForSingleObject(hMutex,INFINITE); switch(dw) // определения способа завершения ожидания

{

case WAIT_OBJECT_0: // процесс успешно завершился printf("\n thread %d: OK",num);

break;

case WAIT_TIMEOUT: // процесс не завершился за 5000 мс, ожидание прервано printf("\n thread %d: Timeout",num);

break;

case WAIT_FAILED: // неправильный вызов функции printf("\n thread %d: Failure",num);

break;

case WAIT_ABANDONED: //событие занято,

// но поток его занявший уже завершился printf("\nthread %d: OK, but with troubles",num);

break;

}

return 0;

}

int main(int argc, char** argv)

{

int i, x[3];

DWORD dwThreadId[3],dw; HANDLE hThread[3];

//создание одноместного семафора с одним свободным местом hSem = CreateSemaphore(NULL,1,1,"SemaphoreStop");

//СОЗДАНИЕ И ОЖИДАНИЕ ЗАВЕРШЕНИЯ ПОТОКОВ, см первый пример пункта

//закрытие описателей событий

CloseHandle(hEvent); return 0;

}

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

139

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

Взаключении приведем код, реализующий решение задачи о кольцевом буфере с помощью функций WinAPI. Из кода видно, что решение ничем не отличается от приведенного в общей нотации в главе 2, и, с точностью до обозначения функций, повторяет реализацию с помощью библиотеки Pthread

вглаве 3.

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

Как уже подробно обсуждалось, задача обладает двумя критическими участками кода. Первая критическая секция связана с операциями чтениязаписи нескольких потоков в общий буфер, она реализована с помощью двух мьютексов (hMutexD, hMutexF). Во втором случае проблема синхронизации определяется тем, что буфер являются конечным, запись должна производиться только в те ячейки, которые являются свободными или уже прочитаны пото- ками-читателями. Ограниченность буфера реализуется парой n-местных семафоров. Значение первого семафора hSemEmpty показывает, сколько ячеек в буфере свободно. Ячейка свободна, когда в нее еще не осуществлялась запись или ячейка была прочитана. Значение второго семафора hSemFull показывает, сколько ячеек в буфере занято. Естественно, операция записи не может быть выполнена, пока количество занятых ячеек равно 100 (или количество свободных ячеек равно 0), и операция чтения не может быть выполнена, пока количество свободных ячеек равно 100 (или количество занятых ячеек равно 0). Ниже приведено решение задачи о кольцевом буфере с помощью функций WinAPI.

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

140

#include <stdio.h> #include <stdlib.h> #include <conio.h> #include <windows.h>

const int n = 100, m = 7, k = 3; int buf[n], front = 0, rear = 0;

HANDLE hSemFull,hSemEmpty, hMutexD, hMutexF;

// процесс, пополняющий буфер

DWORD WINAPI Producer(PVOID pvParam)

{

int num; long prev;

num = *((int *)pvParam);

printf("thread %d (producer): start!\n",num);

while(true)

{

WaitForSingleObject(hSemEmpty,INFINITE);

WaitForSingleObject(hMutexD,INFINITE); buf[rear] = rand()%n;

printf("\nproducer %d: data = %d to %d",num,buf[rear],rear); rear = (rear+1)%n;

Sleep(1000);

ReleaseMutex(hMutexD);

ReleaseSemaphore(hSemFull,1,&prev);

}

return 0;

}

// процесс, берущий данные из буфера

DWORD WINAPI Consumer(PVOID pvParam)

{

int num,data; long prev;

num = *((int *)pvParam);

printf("thread %d (consumer): start!\n",num);

while(true)

{

WaitForSingleObject(hSemFull,INFINITE);

WaitForSingleObject(hMutexF,INFINITE); data = buf[front];

printf("\nconsumer %d: data = %d from %d",num,data,front); front = (front+1)%n;

Sleep(1000);

ReleaseMutex(hMutexF);

ReleaseSemaphore(hSemEmpty,1,&prev);

}

return 0;

}

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