Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
DiVM / OSISP / ОCиСП-Часть2 / Теория / Теория (ОСиСП).doc
Скачиваний:
32
Добавлен:
11.05.2015
Размер:
5.47 Mб
Скачать

Часть V структурная обработка исключений

Глава 23 Обработчики завершения

Закроем глаза и помечтаем, какие бы программы мы писали, если бы сбои в них были невозможны! Представляете, памяти навалом, неверных указателей никто не переда ет, нужные файлы всегда на месте Не программирование, а праздник, да? А код про грамм? Насколько он стал бы проще и понятнее! Без всех этих if иgoto,

И если Вы давно мечтали о такой среде программирования, Вы сразу жс оцените структурную обработку исключений (structured exception handling, SEH). Преимуще ство SEH в том, что при написании кода можно сосредоточиться на решении своей задачи Если при выполнении программы возникнут неприятности, система сама обнаружит их и сообщит Вам.

Хотя полностью игнорировать ошибки в программе при использовании SEH нельзя, она всс жс позволяет отделить основную работу от рутинной обработки оши бок, к которой можно вернуться позже.

Главное, почему Microsoft ввела в Windows поддержку SEH, было ее стремление упростить разработку операционной системы и повысить ее надежность. А нам SЕН поможет сделать надежнее наши программы

Основная нагрузка по поддержке SEH ложится на компилятор, а не на операци онную систему. Он генерирует специальный код на входах и выходах блоков исклю чений (exception blorks), создает таблицы вспомогательных структур данных для поддержки SEH и предоставляет функции обратного вызова, к которым система мог ла бы обращаться для прохода по блокам исключений Компилятор отвечает и за формирование стековых фреймов (stack frames) и другой внутренней информации, используемой операционной системой. Добавить поддержку SEH в компилятор — задача не из легких, поэтому не удивляйтесь, когда увидите, что разные поставщики no-разному реализуют SEH в своих компиляторах К счастью, на детали реализации можно не обращать внимания, а просто задействовать возможности компилятора в поддержке SEH

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

NOTE: Не путайте SEH с обработкой исключении в С++, которая представляет собой еще одну форму обработки исключений, построенную на применении ключе вых слов языка С++ catch и throw При этом Microsoft Visual C++ использует пре имущества поддержки SEH, уже обеспеченной компилятором и операционны ми сиоемдми Windows.

SEH предоставляет две основные возможности, обработку завершения (termination handling) и обработку исключений (exception handling). B этой главе мы рассмотрим обработку завершения.

Обработчик завершения гарантирует, что блок кода (собственно обработчик) будет выполнен независимо от того, как происходит выход из другого блока кода — защищенного участка программы. Синтаксис обработчика завершения при работе с компилятором Microsoft Visual C++ выглядит так:

__try { // защищенный блок }

_finally { // обработчик завершения }

Ключевые слова _try и__flnally обозначают два блока обработчика завершения, В предыдущем фрагменте кода совместные действия операционной системы и ком пилятора гарантируют, что код блока finаllу обработчика завершения будет выполнен независимо от того, как произойдет выход из защищенного блока. И неважно, разме стите Вы в защищенном блоке операторыreturn, goto или дажеlongjump — обработ чик завершения все равно будет вызван. Далее я покажу Вам несколько примеров ис пользования обработчиков завершения

Примеры использования обработчиков завершения

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

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

Funcenstein1

Чтобы оценить последствия применения обработчиков завершения, рассмотрим бо лее конкретный пример:

DWORD Funcenstein1() { DWORD dwTemp; // 1 Что-то делаем здесь

__try { // 2. Запрашиваем разрешение на доступ // к защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; }

_finally { // 3 Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); }

// 4 Продолжаем что-то делать return(dwTemp); }

Пронумерованные комментарии подсказывают, в каком порядке будет выполнять ся этот код. Использование в Funcemtein1 блоковtry-finally на самом деле мало что дает. Код ждет освобождения семафора, изменяет содержимое защищенных данных, сохраняет новое значение в локальной переменнойdivTemp, освобождает семафор и возвращает повое значение тому, кто вызвал эту функцию.

Funcenstein2

Теперь чуть-чуть изменим код функции и посмотрим, что получится:

DWORD Funcenstein2() { DWORD dwTemp; // 1 Что-то делаем здесь

...

__try { // 2 Запрашиваем разрешение на доступ // к защищенным данным, а затем используем их WaitForSingleObject(g_nSem, INFINITE);

g_dwProtectedData = 5; dwTemp = g_dwProlecledData;

// возвращаем новое значение return(dwTemp); }

_finally {

// 3 Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); }

// продолжаем что-то делать - в данной версии // этот участок кода никогда не выполняется dwTemp = 9; return(dwTemp); }

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

Применив обработчик завершения, мы не допустили преждевременного выпол нения оператора return Когдаreturn пытается реализовать выход из блокаtry, компилятор проверяет, чтобы сначала был выполнен код вблоке finally, — причем до того, как операторуreturn в блокеtry будет позволено реализовать выход из функции Вы зовReleaseSemaphore в обработчике завершения (в функцииFuncenstein2) гаранти рует освобождение семафора — поток не сможет случайно сохранить права на се мафор и тем самым лишить процессорного времени все ожидающие этот семафор потоки.

После выполнения блока finаllу функция фактически завершает работу Любой код за блокомfinally не выполняется, поскольку возврат из функции происходит внутри блокаtry. Так что функция возвращает 5 и никогда — 9

Каким же образом компилятор гарантирует выполнение блок finally до выхода из блокаtry? Дело вот в чем. Просматривая исходный текст, компилятор видит, что Вы вставилиreturn внутрь блокаtry Тогда он генерирует код, который сохраняет воз вращаемое значение (в нашем примере 5) в созданной им же временной перемен ной Затем создаст код для выполнения инструкций, содержащихся внутри блокаfinally, — это называется локальной раскруткой (local unwind) Точнее, локальная рас крутка происходит, когда система выполняет блок finаllу из-за преждевременною выхода из блокаtry Значение временной переменной, сгенерированной компилято ром, возвращается из функции после выполнения инструкций в блокеfinаllу

Как видите, чтобы все это вытянуть, компилятору приходится генерировать допол нительный код, а системе — выполнять дополнительную работу На разных типах процессоров поддержка обработчиков завершения реализуется по-разному Напри мер, процессоруА1рhа понадобится несколько сотен или даже тысяч машинных ко манд, чтобы перехватить преждевременный возврат из try и вызвать код блока finаllу Поэтому лучше не писать код, вызывающий преждевременный выход из блокаtry обработчика завершения, — это может отрицательно сказаться на быстродействии программы. Чуть позже мы обсудим ключевое слово _leave, которое помогает избе жать написания кода, приводящего клокальной раскрутке.

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

Заметьте: когда поток управления выходит из блока try естественным образом (как вFuncensfetn1), издержки от вызова блокаfinally минимальны При использовании компилятора Microsofr на процессорах x86 для входаfinаllу при нормальном выхо де изtry исполняется всего одна машинная команда — вряд ли Вы заметите ее влия ние на быстродействие своей программы Но издержки резко возрастут, ссли компи лятору придется генерироватьдополнительный код, а операционной системе — вы полнять дополншельную работу, как вFuncenstetn2

Funcenstein3

Снова изменим код функции:

DWORD Funcenstein3() { DWORD dwTemp;

// 1 Что-то делаем здесь

__try { // 2. Запрашиваем разрешение на доступ // к защищенным данным, а затем используем их

WaitForSingleObject(g_hSem, INFINITE);

g_dwProtectedData = 5; dwTemp = g_dwProtectedData;

// пытаемся перескочить через блок finally goto ReturnValue: }

__finally { // 3. Даем и другим попользоваться защищенными данными

ReleaseSemaphore(g_hSem, 1, NULL); }

dwTemp = 9;

// 4. Продолжаем что-то делать ReturnValue:

return(dwTemp); }

Обнаружив в блоке try функцииFuncenstein3 операторgofo, компилятор генери рует код для локальной раскрутки, чтобы сначала выполнялся блокfinаllу. Но на этот раз послеfinаllуисполняется код, расположенный за меткойRetumValue, так как воз врат из функции не происходит ни в блокеtry, ни в блокеfinally. B итоге функция возвращает 5. И опять, поскольку Бы прервали естественный ход потока управления изtry вfinally, быстродействие программы — в зависимости от типа процессора — может снизиться весьма значительно.

Funcfurter1

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

DWORD Funcfurter1() { DWORD dwTemp;

// 1. Что-то делаем здесь

...

__try { // 2. Запрашиваем разрешение на доступ // к защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); dwTemp = Funcinator(g_dwProtectedData); }

_finally { // 3. Даем и другим попользоваться защищенными данными RelcaseSemaphore(g_hSem, 1, NULL); }

// 4. Продолжаем что-то делать return(dwTemp); }

Допустим, в функции Funcinator, вызванной из блокаtry, — «жучок», из-за которо го возникает нарушение доступа к памяти. Без SEH пользователь в очередной раз уви дел бы самое известное диалоговое окно Application Error. Стоит его закрыть — за вершится и приложение Если бы процесс завершился (из-за пеправильногодоступа к памяти), семафор остался бы занят — соответственно и ожидающие его потоки не получили бы процессорное время. Но вызовReleaseSemaphore в блоке finаllу гаранти рует освобождение семафора, дажс ссли нарушение доступа к памяти происходит в какой-то другой функции.

Раз обработчик завершения — такое мощное средство, способное перехватывать завершение программы из-за неправильного доступа к памяти, можно смело рассчи тывать ична то, что оно также перехватит комбинацииsetjump/longump и элементар ные операторы типаbreak иcontinue.

Проверьте себя: FuncaDoodleDoo

Посмотрим, отгадаете ли Вы, что именно возвращает следующая функция

DWORD FuncaDoodleDoo() { DWORD dwTerrip = 0;

while (dwTemp < 19) {

__try { if (dwTemp == 2) continue; if (dwTemp == 3) break; }

__finally { dwTernp++; }

dwTemp++;

}

dwTemp += 10; return(dwTemp);

}

Проанализируем эту функцию шаг за шагом. Сначала dwTemp приравнивается 0. Код в блокеtry выполняется, но ни одно из условий в операторахifне дает TRUE, и поток управления естественным образом переходит в блокfinаllу,гдеdwTemp увели чивается до 1. Затем инструкция после блокаfinаllу снова увеличивает значениеdwTemp, приравнивая его 2.

На следующей итерации цикла dwTemp равно 2, поэтому выполняется операторcontinue в блокеtry, Без обработчика завершения, вызывающего принудительное вы полнение блокаfinаllу перед выходом изtry, управление было бы передано непосред ственно в начало циклаwhile, значениеdwTemp больше бы не менялось — и мы в бесконечном цикле! В присутствии же обработчика завершения система обнаружи вает, что операторcontinue приводит к преждевременному выходу изtry, и передает управление блокfinаllу. ЗначениеdwTemp в нем увеличивается до 3, но код за этим

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

Теперь обрабатываем третий проход цикла. На этот раз значение выражения в первом if равно FALSE, а во втором — TRUE. Система снова перехватывает нашу по пытку прервать выполнение блокаtry и обращается ккоду finаllу. ЗначениеdwTemp увеличивается до 4. Так как выполнен операторbreak, выполнение возобновляется после тела цикла. Поэтому код, расположенный за блоком finаllу (но в теле цикла), не выполняется. Код, расположенный за телом цикла, добавляет 10 к значениюdwTemp, что дает в итоге 14, — это и есть результат вызова функции. Даже не стану убеж дать Вас никогда не писать такой код, как вFuncaDoodleDoo. Я-то включилcontinue иbreak в середину кода, только чтобы продемонстрировать поведение обработчика завершения.

Хотя обработчик завершения справляется с большинством ситуаций, в которых выход из блока try был бы преждевременным, он не может вызвать выполнение бло каfinally при завершении потока или процесса. ВызовExitThread илиExitProcess сра зу завершит поток или процесс — без выполнения какого-либо кода в блоке finаllу. То же самое будет, если Ваш поток или процесс погибнут из-за того, что некая програм ма вызвалаTerminateThread илиTerminateProcess. Некоторые функции библиотеки С (вродеabort). в свою очередь вызывающиеExitProcess, тоже исключают выполнениеблока finаllу. Раз Вы не можете запретитьдругой программе завершение какого-либо из своих потоков, или процессов, так хоть сами не делайте преждевременных вызо вовExitThread иExitProcess.

Funcenstein4

Рассмотрим еще один сценарий обработки завершения.

DWORD Funcenstein4() {

DWORD dwTemp;

// 1. Что-то делаем здесь

...

__try { // 2. Запрашиваем разрешение на доступ // к защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData;

// возвращаем новое значение return(dwTemp); }

__finally { // 3. Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL);

return(103); }

// продолжаем что-то делать - этот код // никогда не выполняется dwTemp = 9;

return(awTemp); }

Блок try в Funcenstein4 пытается вернуть значение переменнойdwTemp (5) функ ции, вызвавшейFuncenstein4. Как мы уже отметили при обсужденииFuncenstein2, попытка преждевременного возврата из блокаtry приводит к генерации кода, кото рый записывает возвращаемое значение во временную переменную, созданную ком пилятором. Затем выполняется код в блокеfinаllу.Кстати, в этом вариантеFuncenstein2 я добавил в блокfinаllуоператорreturn. Вопрос: что вернетFuncenstein4 — 5 или 103? Ответ: 103, так как операторreturn в блокеfinаllуприведет к записи значения 103 в ту же временную переменную, в которую занесено значение 5. По завершении блокаfinаllу текущее значение временной переменной (103) возвращается функции, вызвав шейFuncenstein4

Итак, обработчики завершения, весьма эффективные при преждевременном вы ходе из блока try, могут дать нежелательные результаты именно потому, что предотв ращают досрочный выход из блокаtry. Лучше всего избегать любых операторов, спо собных вызыать преждевременный выход из блокаtry обработчика завершения. А в идеале — удалить все операторыreturn, continue, break,goto (и им подобные) как из блоковtry, так и из блоковfinally. Тогда компилятор сгенерирует код и более компак-. тный (перехватывать преждевременные выходы из блоковtry не понадобится), и бо лее быстрый (на локальную раскрутку потребуется меньше машинных команд). Да и читать Ваш код будет гораздо легче.

Funcarama1

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

BOOL Funcarama1() {

HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL; DWORD dwNumBytesRead; BOOL fOk;

hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

lf (hFile == INVALID_HANDLE_VALUE) { return(FALSE); }

pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRTTE);

if (pvBuf == NULL) { CloseHandle(hFile); return(FALSE); }

fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);

if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return(FALSE); }

// что-то делаем с данными

...

// очистка всех ресурсов

VirtuallFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle{hFile); return(TRUE);

}

Проверки ошибок в функции Fипсаrата1 затрудняют чтение ее текста, что услож няст ее понимание, сопровождение и модификацию

Funcarama2

Конечно, можно переписать Funcaramal так, чтобы она была яснее:

BOOL Funcarama2() {

HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL; DWORD dwNumByTesRead; BOOL fOk; fSuccess = FALSE;

hFile = CreatcFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ NULL, OPEN_EXISTING, 0, NULL);

if (hFile != INVALID_HANDLE_VALUE) { pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);

if (pvBuf != NULL) {

fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRedd, NULL); if (fOk && (dwNumBytesRead != 0)) { // что-то делаем с данными ... fSuccess = TRUE; }

}

VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);

}

CloseHandle(hFile);

return(fSuccess);

}

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

Funcarama3

Перспишем-ка еще раз первый вариант (Funcaramal), задействовав преимущества обработки завершения

BOOL Funcarama3() { // Внимание! Инициализируйте все переменные, предполагая худшее

HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL;

__try {

DWORD dwNumBytesRead; BOOL fOk;

hFile = CreateFile("SOMEDATA.DAT". GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

if (hFile == INVALID_HANDLE_VALUE) { return(FALSE); }

pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf == NULL) { return(FALSE); }

fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (ifOk || (dwNumBytesRead != 1024)) { return(FALSE); }

// что-то делаем с данными

...

}

__finally {

// очистка всех ресурсов if (pvBuf != NULL) VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);

if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile);

}

// продолжаем что-то делать return(TRUE);

}

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

Funcarama4: последний рубеж

Настоящая проблема в Fипсаrата3 — расплата за изящество. Я уже говорил: избегай те по возможности операторовreturn внутри блокаtry.

Чтобы облегчить последнюю задачу, Microsoft ввела еще одно ключевое слово в свой компилятор С++- _leave. Вот новая версия(Funcarama4), построенная на при менении нового ключевого слова:

BOOL Funcarama4() {

// Внимание, инициализируйте все переменные, предполагая худшее

HANDLE hFile = INVALID_HANDLE_VALUE;

PVOID pvBuf = NULL;

// предполагаем, что выполнение функции будет неудачным BOOL fFunctionOk = FALSE;

__try {

DWORD dwNumBytesRead; BOOL fOk;

hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID,HANDLE_VALUE) { __leave; }

pvBuf = VirtualAlloc(NULL, 1024, MEM_COHMIT, PAGE_READWRITE);

if (pvBuf == NULL) { __leave; }

fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { __leave; }

// что-то делаем с данными // функция выполнена успешно fFunctionOk = TRUE; }

__finally { // очистка всех ресурсов if (pvBuf != NULL) VirtualFree(pvBuf, MEM_RELEASE | MEM__DECOMMIT);

if (hFile != INVALID_HANDLE_VALUE)

CloseHandle(hFile); } // продолжаем что-то делать

return(fFunctionOk); }

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

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

метод отслеживания ресурсов, подлежащих освобождению, — установка флага при успешном выделении ресурса. Код finally проверяет состояние флага и таким обра зом определяет, надо ли освобождать ресурс,

И еще о блоке finally

Пока нам с Вами удалось четко выделить только два сценария, которые приводят к выполнению блока finаllу:

• нормальная передача управления от блока try блоку finаllу;

• локальная раскрутка преждевременный выход из блокаtry (из-за операто ровgoto, longjump, continue, break, return и т. д.), вызывающий принудительную передачу управления блокуfinаllу.

Третий сценарий — глобалъная раскрутка (global unwind) — протекает не столь выраженно. ВспомнимFuncfurterl. Ее блокtry содержал вызов функцииFuncinator. При неверном доступе к памяти вFuncinator глобальная раскрутка приводила к вы полиению блокаfinаllу вFuncfurter1 Но подробнее о глобальной раскрутке мы пого ворим в следующей главе.

Выполнение кода в блоке finаllу всегда начинается в результате возникновения одной из этих трех ситуаций. Чтобы определить, какая из них вызвала выполнение блокаfinаllу, вызовите встраиваемуюфункцию AbnormalTermination

BOOL AbnormalTermination();

Еe можно вызвать только из блока finаllу; она возвращает булево значение, кото рое сообщает, был ли преждевременный выход из блокаtry, связанного с данным блокомfinаllу.Иначе говоря, если управление естественным образом передано изtry вftnally, AbnormalTermination возвращает FALSE. А ссли выход был преждевременным — обычно либо из-за локальной раскрутки, вызванной операторомgoto, return, break илиcontinue, либо из-за глобальной раскрутки, вызванной нарушением доступа к памя ти, — то вызовAbnormalTermination дает TRUE Но, когда она возвращяет TRUE, разли чить, вызвано выполнение блока finаllу глобальной или локалыюй раскруткой, нельзя. Впрочем, это не проблема, так как Вы должны избегать кода, приводящего к локаль ной раскрутке

Funcfurter2

Следующий фрагмент демонстрирует использование встраиваемой функции Abnor malTermination:

DWORD Funcfurter2() {

DWORD dwTemp;

// 1 Что-то делаем здесь

...

1Встраиваемая функция (intrinsic function) — особая функция, распознаваемая компилято ром Вместо генерации вызова такой функции он подставляет в точке вызова ее код. При мером встраиваемой функции являетсяmemcpy (если указан ключ компилятора /Oi). Встре чая вызовmemcpy, компилятор подставляет ec код непосредственно в вызывающую функ цию Обычно это ускоряет работу программы ценой увеличения ее размера ФункцияAbnormalTermination отличается оттетсру тем, что существует только во встраиваемом варианте. Ее нет ни в одной библиотеке С.

__try {

// 2. Запрашиваем разрешение на доступ // к защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE);

dwTemp = Funcinator(g_dwProtectedData);

}

__finally {

// 3. Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL);

if (!AbnormalTermination()) {

// в блоке try не было ошибок - управление // передано в блок finally естественным образом

...

}

else

{

// что-то вызвало исключение, и, так как в блоке try // нет кода, который мог бы вызвать преждевременный // выход, блок finally выполняется из-за глобальной // раскрутки // если бы в блоке try был оператор goto, мы бы // не узнали, как попали сюда

}

}

// 4. Продолжаем что-то делать

return(dwТemp);

}

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

  • Упрощается обработка ошибок — очистка гарантируется и проводится в од ном месте.

  • Улучшается восприятие текста программ.

  • Облегчается сопровождение кода.

  • Удается добиться минимальных издержек по скорости и размеру кода — при условии правильного применения обработчиков.

Программа-пример SEHTerm

Эта программа, «23 SEHTerm.exe» (см. листинг на рис. 23-1), демонстрирует обработ чики завершения. Файлы исходного кода и ресурсов этой программы находятся в каталоге 23-SEHTerm на компакт-диске, прилагаемом к книге.

После запуска SEHTerm ее первичный поток входит в блок try. Из него открывает ся следующее окно.

В этом окне предлагается обратиться к памяти по недопустимому адресу. (Боль шинство приложений не столь тактично — они обращаются по недопустимым адре сам, никого не спрашивая.) Давайте обсудим, что случится, если Вы щелкнете кнопку Yes. B этом случае поток попытается записать значение 5 по нулевому адресу памяти. Запись по нулевому адресу всегда вызывает исключение, связанное с нарушением доступа. А когда поток возбуждает такое исключение, Windows 98 выводит окно, по казанное ниже.

В Windows 2000 аналогичное окно выглядит иначе

Если Вы теперь щелкнитe кнопку Сlоsе (в Windows 98) или OK (в Windows 2000), процесс завершится. Однако в исходном коде этой программы присутствует блок finally, который будет выполнен до того, как процесс завершится Из этого блока от крывается следующее окно.

Блок finаllу выполняется потому, что происходит ненормальный выход из связан ного с пим блокаtry. После закрытия этого окна процесс завершается.

О'кэй, а сейчас снова запустим эту программу. Но на этот раз попробуйте щелк нуть кнопку No, чтобы избежать обращения к памяти по недопустимому адресу. Тог да поток естественным образом перейдет из блока try вблок finаllу, откуда будет от крыто следующее окно.

Обратите внимание, что на этот раз в окне сообщается о нормальном выходе из блока try Когда Вы закроете это окно, поток выйдет из блокаfinаllуи покажет после днее окно.

Послетою как Вы скроете и это окно, процесс нормально завершится, посколь ку функция WinMain вернет управление. Заметьте, что данное окно не появляется при аварийном завершении процесса.