Объекты синхронизации и функции ожидания

В операционных системах Windows объектами синхронизации называются объекты ядра, которые могут находиться в одном из двух состояний: сигнальном (signaled) и несигнальном (nonsignaled). Объекты синхронизации могут быть разбиты на четыре класса.

К первому классу относятся собственно объекты синхронизации, т. е. те, которые служат только для решения задач синхронизации параллельных потоков. К таким объектам синхронизации в Windows относятся:

□ мьютекс (mutex);

□ событие (event);

□ семафор (semaphore).

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

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

□ работа (Job);

□ процесс (process);

□ поток (thread).

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

□ изменение состояния каталога (change notification);

□ консольный ввод (console input).

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

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

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

DWORD WaitForsingieObject(

HANDLE hHandle, // дескриптор объекта

DWORD dwMilliseconds // интервал ожидания в миллисекундах

);

Функция WaitForsingieObject в течение интервала времени, равного значению параметра dwMilliseconds, ждет перехода объекта синхронизации, дескриптор которого задается параметром hHandle, в сигнальное состояние. Если значение параметра dwMilliseconds равно нулю, то функция только проверяет состояние объекта синхронизации. Если же значение параметра dwMilliseconds равно infinite, то функция ждет перехода объекта синхронизации в сигнальное состояние бесконечно долго.

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

□ wait_object_o — объект перешел в сигнальное состояние;

□ wait_abandoned — забытый мьютекс;

□ wait_timeout — время ожидания истекло.

Значение wait_object_o означает, что объект синхронизации находился или перешел в сигнальное состояние. Значение wait_abandoned означает, что объектом синхронизации являлся мьютекс, который не освободился завершившимся потоком. В этом случае мьютекс освобождается операционной системой и поэтому также переходит в сигнальное состояние. Такой мьютекс иногда называется забытый или заброшенный мьютекс (abandoned mutex). Значение wait_timeout означает, что время ожидания истекло, а объект синхронизации так и не перешел в сигнальное состояние. В случае неудачи функция waitForsingieObject возвращает значение WAIT_FAILED.

Мьютексы

Для решения проблемы взаимного исключения между параллельными потоками, выполняющимися в контекстах разных процессов, в операционных системах Windows используется объект ядра мьютекс. Слово мьютекс происходит от английского слова mutex, которое в свою очередь является сокращением от выражения mutual exclusion, что на русском языке значит "взаимное исключение". Мьютекс находится в сигнальном состоянии, если он не принадлежит ни одному потоку. В противном случае мьютекс находится в несигнальном состоянии. Одновременно мьютекс может принадлежать только одному потоку.

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

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

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes, // атрибуты защиты

BOOL blnitialOwner, // начальный владелец мьютекса

LPCTSTR lpName // имя мьютекса

);

Пока значение параметра lpsecurity_attributes будем устанавливать в null. Это означает, что атрибуты защиты заданы по умолчанию, т. е. дескриптор мьютекса не является наследуемым и доступ к мьютексу открыт для всех пользователей. Теперь перейдем к другим параметрам. Если значение параметра blnitialOwner равно true, to мьютекс сразу переходит во владение потоку, которым он был создан. В противном случае вновь созданный мьютекс свободен. Поток, создавший мьютекс, имеет все права доступа к этому мьютексу.

Значение параметра lpName определяет уникальное имя мьютекса для всех процессов, выполняющихся под управлением операционной системы. Это имя позволяет обращаться к мьютексу из других процессов, запущенных под управлением этой же операционной системы. Длина имени не должна превышать значение мах_ратн. Значением параметра lpName может быть пустой указатель null. В этом случае система создает безымянный мьютекс. Отметим также, что имена мьютексов являются чувствительными к нижнему и верхнему регистрам.

В случае удачного завершения функция CreateMutex возвращает дескриптор созданного мьютекса. В случае неудачи эта функция возвращает значение null. если мьютекс с заданным именем уже существует, то функция CreateMutex возвращает дескриптор этого мьютекса, а функция GetLastError, вызванная после функции CreateMutex, вернет значение ERROR_ALREADY_EXISTS.

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

BOOL ReleaseMutex (

HANDLE hMutex // дескриптор мьютекса

);

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

Семафоры

Семафоры в операционных системах Windows описываются объектами ядра semaphores. Семафор находится в сигнальном состоянии, если его значение больше нуля. В противном случае семафор находится в несигнальном состоянии. Потоки, ждущие сигнального состояния семафора, обслуживаются в порядке FIFO, т. е. потоки становятся в очередь к семафору с дисциплиной обслуживания "первый пришел, первый вышел". Однако если поток ждет наступления асинхронного события, то функции ядра могут исключить поток из очереди к семафору для обслуживания наступления этого события. После этого поток становится в конец очереди семафора.

Создаются семафоры посредством вызова функции createsemaphore, которая имеет следующий прототип:

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES IpSemaphoreAttribute, // атрибуты защиты

LONG llnitialCount, // начальное значение семафора

LONG IMaximumCount, // максимальное значение семафора

LPCTSTR lpName // имя семафора

);

Пока значение параметра lpSemaphoreAttributes будем устанавливать в null. Основную смысловую нагрузку в этой функции несут второй и третий параметры. Значение параметра linitialcount устанавливает начальное значение семафора, которое должно быть не меньше 0 и не больше его максимального значения, которое устанавливается параметром IMaximumCount.

Параметр lpName может указывать на имя семафора или содержать значение null. В последнем случае создается безымянный семафор. В случае успешного завершения функция createsemaphore возвращает дескриптор семафора, в случае неудачи — значение null. Если семафор с заданным именем уже существует, то функция createsemaphor возвращает дескриптор этого семафора, а функция GetLastError, вызванная после функции Createsemaphor, вернет значение ERROR_ALREADY_EXISTS.

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

BOOL ReleaseSemaphore(

HANDLE hSemaphore, // дескриптор семафора

LONG IReleaseCount, // положительное число, на которое

// увеличивается значение семафора

LPLONG lpPreviousCount // предыдущее значение семафора

);

В случае успешного завершения функция ReleaseSemaphore возвращает ненулевое значение, а в случае неудачи — false. Если значение семафора плюс значение параметра IReleaseCount больше максимального значения семафора, то функция ReleaseSemaphore возвращает значение false и значение семафора не изменяется. Значение параметра lpPreviousCount этой функции может быть равно null. В этом случае предыдущее значение семафора не возвращается.

Доступ к существующему семафору можно открыть с помощью одной из функций CreateSemaphore или OpenSemaphore. если для этой цели используется функция CreateSemaphore, то значения параметров UnitialCount и Maximalcount этой функции игнорируются, т. к. они уже установлены другим потоком, а поток, вызвавший эту функцию, получает полный доступ к семафору с именем, заданным параметром lpName.

Лекция 11. Структурная обработка исключений

Исключения и их обработчики

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

В операционных системах Windows для этой цели предназначен механизм структурной обработки исключений (structured exception handling, SEH). Смысл механизма структурной обработки исключений заключается в следующем.

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

Очевидно, что для того чтобы использовать этот механизм в программе, в язык программирования C++ нужно ввести новые ключевые слова. Такими ключевыми словами являются try и except, которые расширяют список стандартных ключевых слов языка программирования C++ и различаются только компилятором фирмы Microsoft. Ключевое слово try отмечает фрейм, а ключевое слово except отмечает обработчик исключения. В результате фрагмент программы, который использует механизм структурной обработки исключений, выглядит следующим образом:

_try

{

// охраняемый код

}

except (выражение-филь тр)

{

// код обработки исключения

}

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

□ exception_execute_handler — управление передается обработчику исключений;

□ exception_continue_search — система продолжает поиск обработчика исключения;

□ exception_continue_execution — система передает управление в точку прерывания программы.

Сделаем два важных замечания относительно механизма структурной обработки исключений. Во-первых, не допускается использование оператора goto для передачи управления внутрь фрейма или обработчика исключения. Во-вторых, в выражении фильтра допускается использование функций GetExceptionCode И GetExceptionlnformation, которые предоставляют информацию о происшедшем исключении.

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

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

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

Листинг. Обработка исключения

# include <windows.h>

#include <iostream.h>

int main()

{

int a = 10;

int *р = NULL; // пустой указатель на целое число

—try

{

cout « "a = " « *р « endl; // ошибка, так как р = NULL

}

except(EXCEPTION_EXECUTE_HANDLER)

{

cout « "There was some exception." « endl;

P = &a;

}

cout « "a = " « *p « endl; // нормально

return 0;

}

Концептуально механизм структурной обработки исключений в Windows немного отличается от механизма обработки исключений, принятого в языке программирования C++. Дело в том, что механизм структурной обработки исключений был разработан раньше, чем принят стандарт языка C++. Кроме того, в отличие от языка программирования C++ механизм структурной обработки исключений ориентирован не только на обработку программных исключений, но и на обработку аппаратных исключений. В SEH исключение рассматривается как ошибка, происшедшая при выполнении программы. В языке программирования C++ используется более абстрактный подход и исключение рассматривается как объект произвольного типа, который может выбросить программа, используя оператор throw. В свою очередь обработчик исключения catch может рассматриваться как функция с одним параметром, которая выполняется только в том случае, если тип ее параметра соответствует типу выброшенного исключения.

Получение кода исключения

Получить код происшедшего исключения можно при помощи функции GetExceptionCode, которая имеет следующий прототип:

DWORD GetExceptionCode(VOID);

Функция GetExceptionCode возвращает одно из следующих значений:

□ exception_access_violation — попытка чтения или записи в виртуальную память без соответствующего права доступа;

□ exception_breakpoint — встретилась точка останова;

□ exception_datatype_misalignment — доступ к данным, адрес которых не выровнен по границе слова или двойного слова;

□ exception_single_step — механизм трассировки программы сообщает, что выполнена одна инструкция;

□ exception_array_biunds_exceeded — выход за пределы массива, если аппаратное обеспечение поддерживает такую проверку;

□ exception_flt_denormal_operand — один из операндов с плавающей точкой является ненормализованным;

□ exception_flt_divide_by_zero — попытка деления на ноль в операции с плавающей точкой;

□ exception_flt_inexact_result — результат операции с плавающей точкой не может быть точно представлен десятичной дробью;

□ exception_flt_invalid_operation — ошибка в операции с плавающей точкой, для которой не предусмотрены другие коды исключения;

□ exception_flt_overflow — при выполнении операции с плавающей точкой произошло переполнение;

□ exception_flt_stack_check — переполнение или выход за нижнюю границу стека при выполнении операции с плавающей точкой;

□ exception_flt_underflow — результат операции с плавающей точкой является числом, которое меньше минимально возможного числа с плавающей точкой;

□ exception_int_divide_by_zero — попытка деления на ноль в операции с целыми числами;

□ exception_int_overflow — при выполнении операции с целыми числами произошло переполнение;

□ exception_priv_instruction — попытка выполнения привилегированной инструкции процессора, которая недопустима в текущем режиме процессора;

□ exception _noncontinuable_exception — попытка возобновления исполнения программы после исключения, которое запрещает выполнять такое действие.

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

Наши рекомендации