Новые книги

Наша эра неограниченного доступа к информации – лучшее время, чтобы начать свое дело. Тем более, что маркетинг теперь стал… бесплатным.

Каждый день в мире появляются прибыльные проекты, которые не требуют крупных вложений в рекламу. И каждый день рождаются интереснейшие идеи эффективного маркетинга. Чтобы запустить проект мечты, не обязательно читать эту книгу от корки до корки, можете позаимствовать самые сливки. Вам подойдут не все идеи, но скорее всего, подойдет большинство. Все они крайне экономичны, если не бесплатны, – и, главное, эффективны. Большинство стратегий касаются использования интернета как мощного средства маркетинга, однако есть и те, что не связаны с интернетом.
Instagram сегодня – самая популярная и удобная площадка, с помощью которой можно стать действительно знаменитым. Петр Плосков – Instagram-продюсер № 1 в России, занимающийся продвижением Дмитрия Маликова, Натальи Рудовой, Марии Миногаровой, Иды Галич, Ольги Медынич, Ники Вайпер и других знаменитостей с миллионными аудиториями. В своей книге Петр рассказывает обо всем, что необходимо знать, чтобы стать следующей звездой.

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



ЧАСТЬ 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. Из него открывает ся следующее окно.

rihter23-1.jpg

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

rihter23-2.jpg

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

rihter23-3.jpg

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

rihter23-4.jpg

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

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

rihter23-5.jpg

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

rihter23-6.jpg

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

SEHTerm