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

Операционные Системы

.pdf
Скачиваний:
37
Добавлен:
02.03.2016
Размер:
1.94 Mб
Скачать

Лабораторная работа № 5. Синхронизация.

Цель занятия

определение типичных задач синхронизации

ознакомление с принципами синхронизации потоков

научиться

-решать задачи взаимного исключения с использованием критических секций и объекта ядра «мьютекс» и «семафор»;

-решать задачи типа производитель/потребитель и читатель/писатель с использованием объекта ядра «событие»;

-выявлять аномалии в синхронизации: эффект гонок, тупиковые ситуации.

Теория

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

Суть синхронизации. В общем случае поток синхронизирует себя с другим потоком так: он засыпает, и операционная система, не выделяя ему процессорного времени, приостанавливает его выполнение. Но прежде чем заснуть, поток сообщает системе, какое особое событие должно произойти, что бы его выполнение возобновилось. Как только указанное событие произойдѐт, поток вновь получит процессорное время (продолжит выполнение). Таким образом, выполнение потока синхронизировано с определѐнным событием.

Если бы синхронизирующих объектов не было, потоку пришлось бы самому пришлось отслеживать определѐнные события и синхронизировать себя с ними. Далее приводится пример программы, где потоки синхронизируются без использования заложенных в систему механизмов (никогда не пишите такие программы). Суть приѐма в том, что поток синхронизирует себя с завершением какой-либо задачи в другом потоке, постоянно просматривая значение переменной, доступной обоим потокам ( Пример 14).

51

Пример 14. Плохой метод синхронизации потоков в режиме пользователя.

//Переменная принимает значение TRUE когда заканчивается выполнение

//пересчѐта

BOOL g_Flag = FALSE;

.. Функция что-то считает

DWORD __stdcall SomeRecal(LPVOID)

{

g_Flag = TRUE; // пересчѐт закончен return 0;

}

int main()

{

CreateThread(…, RecalcFunc, …);

while (!g_Flag); // Ждѐм завершения пересчѐта

return 0;

}

Минусы приѐма:

Потоку, ожидающему завершения пересчѐта выделяется процессорное время.

Если у потока ожидающего завершения пересчѐта более высокий приоритет, то поток пересчѐта никогда не получит процессорного

времени.

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

Задача взаимного исключения

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

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

52

Двум и более задачам (потокам) одновременно записывать данные в хранилище, что приводит к несогласованности данных.

Для решения подобных проблем ОС предоставляет несколько синхронизирующих механизмов.

Критическая секция (КС)

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

Таблица 6. Функции управления КС

Функция

Назначение

 

 

InitializeCriticalSection(LPCRITICAL_SECTION

Инициализация КС.

cs)

Должно быть сделано до

 

первого использования.

 

 

DeleteCriticalSection(LPCRITICAL_SECTION

Удаление КС. Обычно

cs)

после завершения

 

потоков, использующих

 

КС.

 

 

EnterCriticalSection(LPCRITICAL_SECTION cs)

Вход в КС. За вызовом

 

этой функции начинается

 

защищѐнная область кода.

 

 

LeaveCriticalSection(LPCRITICAL_SECTION

Выход из КС. За вызовом

cs)

этой функции следует

 

незащищѐнный код

 

программы.

 

 

Пример 15. Решение задачи взаимного исключения с помощью КС

 

 

 

int i=0;

 

CRITICAL_SECTION cs;

 

DWORD __stdcall IncThread(LPVOID)

 

{

 

 

 

53

for (;;)

{

EnterCriticalSection(&cs);

i+=10;

Sleep(0); i-=10; Sleep(0);

printf("%d ", i); LeaveCriticalSection(&cs);

}

}

int _tmain(int argc, _TCHAR* argv[])

{

HANDLE h[2];

InitializeCriticalSection(&cs);

h[0] = CreateThread(NULL, 0, IncThread, NULL, 0, NULL);

h[1] = CreateThread(NULL, 0, IncThread, NULL, 0, NULL);

WaitForMultipleObjects(2, h, TRUE, 1000); TerminateThread(h[0], 0); TerminateThread(h[1], 0); CloseHandle(h[0]);

CloseHandle(h[1]);

DeleteCriticalSection(&cs); return 0;

}

Пример 15 демонстрирует решение типичной задачи взаимного исключения, когда имеется один разделяемый ресурс (переменная i) и два потока, работающие с переменной. Функция потока выполняет приращение переменной на 10 и последующее уменьшение значения на это же число. Т.о. распечатка переменной на экране должна давать значение «0». При

54

отсутствии КС значение переменной i было бы в большей степени неопределѐнным.

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

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

Каждый объект ядра может находится в одном из двух состояний:

свободном (signaled) или занятом (nonsignaled). Потоки могут останавливаться и ждать освобождения какого-либо объекта. Если, скажем, поток родительского процесса должен ждать завершения дочернего, его можно отправить «в спячку» вплоть до освобождения объекта ядра, идентифицирующего дочерний процесс.

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

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout);

и

DWORD WaitForMultipleObjects(DWORD cObjects, LPHANDLE lpHandles,

BOOL bWaitAll, DWORD dwTimeout);

WaitForSingleObject сообщает системе, что поток ожидает освобождения ядра, на который указывает параметр hObject. Параметр dwTimeout показывает, сколько времени (в миллисекундах) поток готов ждать этого события. Если объект ядра за заданное время не перейдѐт в свободное состояние, система вновь активизирует поток и продолжит его исполнение.

WaitForSingleObject возвращает одно из следующих значений:

55

Таблица 7. Возвращаемые значения функций WaitForSingleObject и WaitForMultipleObjects

Код

Описание

 

 

WAIT_OBJECT_0

Объект перешѐл в свободное состояние.

 

 

WAIT_TIMEOUT

Объект не перешѐл в свободное состояние за

 

заданное время.

 

 

WAIT_ABANDONED

Объект-мьютекс перешѐл в свободное состояние из-

 

за отказа от него (был разрушен в другом потоке).

 

 

WAIT_FAILED

Произошла ошибка.

 

 

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

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

Объект-мьютекс

Выполняет те же задачи, что и КС, но является объектом ядра и позволяет синхронизировать выполнение потоков, принадлежащих разным процессам (через создание именованного объекта ядра «мьютекс» (см. практику №1. «Объекты ядра Win32»); однако, в нашем курсе ОС этот вопрос не рассматривается. Кроме того, переводя мьютекс в несигнальное состояние поток становится его владельцем. Функции работы с мьютексом даны ниже (см. Таблица 8).

Таблица 8. Функции работы с мьютексом.

Функция

 

Назначение

 

 

 

HANDLE CreateMutex

 

Создаѐт новый объект «мьютекс».

(

 

lpSD – атрибуты защиты (NULL по

LPSECURITY_ATTRIBUTES lpSD,

 

умолч.)

BOOL InitOwner, LPCTSTR lpName

 

bInitOwner – TRUE, если

)

 

создающий поток владеет

 

 

мьютексом.

 

 

lpName – имя объекта для

 

 

 

 

56

 

 

разделения

между

 

процессами.

 

Может быть NULL.

 

 

 

 

 

 

 

BOOL ReleaseMutex(HANDLE

Переводит

мьютекс

в

свободное

hMutex)

состояние.

Вызвать

эту функцию

 

может

только

 

владеющий

 

мьютексом поток.

 

 

 

 

 

 

BOOL CloseHandle(HANDLE

Закрыть

описатель

объекта

hMutext)

«мьютекс».

 

 

 

 

 

 

 

 

Пример 16. Использование мьютекса для решения задачи взаимного исключения.

volatile int i = 0; // Глобальная переменная, которой пользуются оба потока.

// создание объекта «мьютекс». Он должен быть видимым из обоих потоков, т.е. глобальным

static HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);

// Пара функций WaitForSingleObject ReleaseMutex

выполняют те же функции, что и // EnterLeaveCriticalSection с использованием КС. DWORD __stdcall IncThread(LPVOID)

{

for (;;)

{

WaitForSingleObject(hMutex, INFINITE); i+=10;

Sleep(0); i-=10; Sleep(0);

printf("%d ", i); ReleaseMutex(hMutex);

}

}

int mian()

{

HANDLE hThreads[2];

// Создание потоков

hThreads[0] = CreateThread(…,IncThread,…);

57

hThreads[1] = CreateThread(…,IncThread,…);

//Ждѐм окончания потоков

WaitForMultipleObjects(…,hThreads,…);

//Закрываем описатели потоков.

CloseHandle(hThreads[0]);

CloseHandle(hThreads[1]);

//Разрушение мьютекса

CloseHandle(hMutex);

return 0;

}

Объект-семафор

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

Таблица 9. Функции работы с семафорами.

Функция

Назначение

 

 

HANDLE CreateSemaphore(

Создаѐт объект ядра «семафор»

LPSECURITY_ATTRIBUTES

lpSD – атрибуты защиты. NULL – для защиты по

 

умолчанию.

lpSD, LONG lInitialCount,

lInitialCount – количество доступных ресурсов в

 

 

момент создания.

LONG lMaximumCount,

lMaximumCount – всего ресурсов.

 

LPCTSTR lpName

lpName – название объекта для разделения между

 

)

процессами. Может быть NULL.

 

 

 

BOOL ReleaseSemaphore(

Освобождает семафор.

HANDLE hSem,

hSem – семафор.

LONG lRelease,

lRelease – число освобождаемых ресурсов.

PLONG lpPrev

lpPrev – указатель на переменную, куда

)

записывается число доступных ресурсов до вызова

 

функции.

 

 

 

58

Пример 17. Использование семафора.

//Количество экземпляров книги в читальном зале.

#define N 3

//Создаѐм семафор

static HANDLE hSem = CreateSemaphore(NULL, N, N, NULL);

// Функция для использования книги читателем.

DWORD __stdcall UseTheBook(LPVOID)

{

for (;;)

{

//Получаем книгу в читальном зале

//Если книги кончились, то ждѐм бесконечно

долго.

WaitForSingleObject(hSem, INFINITE);

//Используем книгу

Sleep(2000);

//Отдаѐм книгу

ReleaseSemaphore(hSem, 1, NULL);

//Осмысливаем книгу дома

Sleep(5000);

}

}

int _tmain(int argc, _TCHAR* argv[])

{

// 5 посетителей библиотеки. HANDLE h[5];

for (int i=0; i<5; i++)

h[i] = CreateThread(NULL, 0, UseTheBook, NULL,

0,

NULL);

WaitForMultipleObjects(5, h, TRUE, 10000);

for (int i=0; i<5; i++)

{

59

TerminateThread(h[i], 0); CloseHandle(h[i]);

}

return 0;

}

Задача производитель/потребитель

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

Объект–событие

События обычно применяются для уведомления об окончании какихлибо операций (выше рассмотренные объекты служат для контроля за доступом к ресурсам). Существуют два вида событий: события с

автосбросом (auto-reset events) и сбросом вручную (manual - reset events).

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

Таблица 10. Функции работы с объектом «Событие».

Функция

Назначение

 

 

HANDLE CreateEvent(

Создаѐт объект «событие»

LPSECURITY_ATTRIBUTES

lpSD – атрибуты защиты. NULL – для

lpSD,

защиты по умолчанию.

BOOL fManualReset,

fManualReset – TRUE для события со

BOOL fInitialState,

сбросом вручную.

LPCTSTR lpName

fInitialState – TRUE, если в сигнальном

)

состоянии

 

lpName – название объекта для разделения

 

между процессами. Может быть NULL.

 

 

BOOL SetEvent(

Переводит событие со сбросом в ручную в

HANDLE hEvent

сигнальное состояние.

 

)

 

 

 

 

60