Хабрахабр

[DotNetBook] Время занимательных историй: исключительно исключительные ситуации

Причем если попытаться их классифицировать, то как и было сказано в самом начале главы, есть исключения родом из самого . Существует ряд исключительных ситуаций, которые скажем так… Несколько более исключительны чем другие. Их в свою очередь можно разделить на две подкатегории: иcключительные ситуации ядра CLR (которое по своей сути — unsafe) и любой unsafe код внешних библиотек. NET приложения, а есть исключения родом из unsafe мира.

Давайте поговорим про эти особые исключительные ситуации.

ThreadAbortException

Вообще, это может показаться не очевидным, но существует четыре типа Thread Abort.

Полный цикл:
— Архитектура системы типов
— Cобытия об исключительных ситуациях
— Виды исключительных ситуаций (эта статья)
— Сериализация и блоки обработки
Данная статья — третья из четырех в цикле статей про исключения.

  • Грубый вариант ThreadAbort, который, отрабатывая не может быть никак остановлен и который не запускает обработчиков исключительных ситуаций вообще включая секции finally
  • Вызов метода Thread.Abort() на текущем потоке
  • Асинхронное исключение ThreadAbortException, вызванное из другого потока
  • Если во время выгрузки AppDomain существуют потоки, в рамках которых запущены методы, скомпилированные для этого домена, будет произведен ThreadAbort тех потоков, в которых эти методы запущены

NET Framework, однако его не существует на CoreCLR или же под Windows 8 "Modern app profile". Стоит заметить что ThreadAbortException довольно часто используется в большом . Попробуем узнать, почему.

второй, третий и четвертый вариант), виртуальная машина при возникновении такого исключения начинает идти по всем обработчикам исключительных ситуаций и искать как обычно те, тип исключения которых является тем, что было выброшено либо более базовым. Итак, если мы имеем дело с не принципиальным типом обрыва потока, когда мы еще можем с ним что-то сделать (т.е. Даже int). В нашем случае это три типа: ThreadAbortException, Exception и object (помним что Exception — это по своей сути — хранилище данных и тип исключения может быть любым. В целом, ситуации в двух примерах, описанных ниже абсолютно одинаковые: Отрабатывая все подходящие catch блоки виртуальная машина пробрасыват ThreadAbortException дальше по цепочке обработки исключений попутно входя во все finally блоки.

var thread = new Thread(() =>
catch (Exception ex) { // ... }
});
thread.Start();
//...
thread.Abort(); var thread = new Thread(() =>
{ try { // ... } catch (Exception ex) { // ... if(ex is ThreadAbortException) { throw; } }
});
thread.Start();
//...
thread.Abort();

Тогда может возникнуть понятное желание его все-таки обработать. Конечно же, всегда возникнет ситуация, когда возникающий ThreadAbort может быть нами вполне ожидаем. ResetAbort(), который делает именно то, что нам нужно: останавливает сквозной проброс исключения через всю цепочку обработчиков, делая его обработанным: Как раз для таких случаев был разработан и открыт метод Thread.

void Main()
{ var barrier = new Barrier(2); var thread = new Thread(() => { try { barrier.SignalAndWait(); // Breakpoint #1 Thread.Sleep(TimeSpan.FromSeconds(30)); } catch (ThreadAbortException exception) { "Resetting abort".Dump(); Thread.ResetAbort(); } "Catched successfully".Dump(); barrier.SignalAndWait(); // Breakpoint #2 }); thread.Start(); barrier.SignalAndWait(); // Breakpoint #1 thread.Abort(); barrier.SignalAndWait(); // Breakpoint #2
} Output:
Resetting abort
Catched successfully

И стоит ли обижаться на разработчиков CoreCLR что там этот код попросту выпилен? Однако реально ли стоит этим пользоваться? Когда вы хотите оборвать жизнь потока все чего вы хотите — чтобы он действительно завершил свою работу. Представьте что вы — пользователь кода, который по вашему мнению "повис" и у вас возникло непреодолимое желание вызвать для него ThreadAbortException. Обычно внешний алгоритм решает дождаться корректного завершения операций. Мало того, редкий алгоритм просто обрывает поток и бросает его, уходя к своим делам. Тут в общем не скажешь, что хуже. Или же наоборот: может решить что поток более уже ничего делать не будет, декрементирует некие внутренние счетчики и более не будет завязываться на то что есть какая-то многопоточная обработка какого-либо кода. Сами посудите: вы бросаете ThreadAbort не прямо сейчас а в любом случае спустя некоторое время после осознания безвыходности ситуации. Я даже так вам скажу: отработав много лет программистом я до сих пор не могу вам дать прекрасный способ его вызова и обработки. вы можете как попасть по обработчику ThreadAbortException так и промахнуться мимо него: "зависший код" мог оказаться вовсе не зависшим, а попросту долго работающим. Т.е. Т.е. И как раз в тот момент, когда вы хотели оборвать его жизнь, он мог вырваться из ожидания и корректно продолжить работу. ResetAbort(); }. без лишней лирики выйти из блока try-catch(ThreadAbortException) { Thread. Оборванный поток, который ни в чем не виноват. Что мы получим? Шла уборщица, выдернула провод, сеть пропала.

Хорошо? Метод ожидал таймаута, уборщица вернула провод, все заработало, но ваш контролирующий код не дождался и убил поток. Как-то можно защититься? Нет. Но вернемся к навящивой идее легализации Thread. Нет. Во-первых становится не понятно как его оборвать в таком случае. Abort(): мы кинули кувалдой в поток и ожидаем что он с вероятностью 100% оборвется, но этого может не произойти. Что тогда? Ведь тут все может быть намного сложнее: в подвисшем потоке может быть такая логика, которая перехватывает ThreadAbortException, останавливает его при помощи ResetAbort, однако продолжает висеть из-за сломанной логики. Interrupt()? Делать безусловный thread. Плюс, я вам гарантирую что у вас поплывут утечки: thread. Попахивает попыткой обойти ошибку в логике программы грубыми методами. Также прошу заметить что в случае промаха ThreadAbortException мимо catch(ThreadAbortException) { Thread. Interrupt() не будет заниматься вызовом catch и finally, а это значит что при всем опыте и сноровке очистить ресурсы вы не сможете: ваш поток просто исчезнет, а находясь в соседнем потоке вы можете не знать ссылок на все ресурсы, которые были заняты умирающим потоком. ResetAbort(); } у вас точно также потекут ресурсы.

И это будет совершенно правильная мысль: это будет доказательством того что пользоваться Thread. После того что вы прочитали чуть выше я надеюсь, вы остались в некотором состоянии запутанности и желания перечитать абзац. Как и нельзя пользоваться thread. Abort() попросту нельзя. Оба метода приводят к неконтролируемому поведению вашего приложения. Interrupt();. NET Framework. По своей сути они нарушают принцип целостности: основной принцип .

NET Framework и найти места использования Thread. Однако, чтобы понять для каких целей этот метод введен в эксплуатацию достаточно посмотреть исходные коды . Ведь именно его наличие по сути легализует thread. ResetAbort(). Abort().

Класс ISAPIRuntime ISAPIRuntime.cs

try { // ... }
catch(Exception e) { try { WebBaseEvent.RaiseRuntimeError(e, this); } catch {} // Have we called HSE_REQ_DONE_WITH_SESSION? If so, don't re-throw. if (wr != null && wr.Ecb == IntPtr.Zero) { if (pHttpCompletion != IntPtr.Zero) { UnsafeNativeMethods.SetDoneWithSessionCalled(pHttpCompletion); } // if this is a thread abort exception, cancel the abort if (e is ThreadAbortException) { Thread.ResetAbort(); } // IMPORTANT: if this thread is being aborted because of an AppDomain.Unload, // the CLR will still throw an AppDomainUnloadedException. The native caller // must special case COR_E_APPDOMAINUNLOADED(0x80131014) and not // call HSE_REQ_DONE_WITH_SESSION more than once. return 0; } // re-throw if we have not called HSE_REQ_DONE_WITH_SESSION throw;
}

Т.е. В данном примере происходит вызов некоторого внешнего кода и если тот был завершен не корректно: с ThreadAbortException, то при определенных условиях помечаем поток как более не прерываемый. Почему в данном конкретно случае мы обрываем Thread. по сути обрабатываем ThreadAbort. Потому что в данном случае мы имеем дело с серверным кодом, а он в свою очередь вне зависимости от наших ошибок вернуть корректные коды ошибок вызывающей стороне. Abort? Также оставлен комментарий о Thread. Обрыв потока привел бы к тому что сервер не смог бы вернуть нужный код ошибки пользователю, а это совершенно не правильно. Unload(), что является экстримальной ситуацией для ThreadAbort поскольку такой процесс не остановить и даже если вы сделаете Thread. Abort() во время AppDomin. Это хоть и остановит сам Abortion, но не остановит выгрузку потока с доменом, в котором он находится: поток же не может исполнять инструкции кода, загруженного в домен, который отгружен. ResetAbort.

Класс HttpContext HttpContext.cs

internal void InvokeCancellableCallback(WaitCallback callback, Object state) { // ... try { BeginCancellablePeriod(); // request can be cancelled from this point try { callback(state); } finally { EndCancellablePeriod(); // request can be cancelled until this point } WaitForExceptionIfCancelled(); // wait outside of finally } catch (ThreadAbortException e) { if (e.ExceptionState != null && e.ExceptionState is HttpApplication.CancelModuleException && ((HttpApplication.CancelModuleException)e.ExceptionState).Timeout) { Thread.ResetAbort(); PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_TIMED_OUT); throw new HttpException(SR.GetString(SR.Request_timed_out), null, WebEventCodes.RuntimeErrorRequestAbort); } }
}

Здесь приведен прекрасный пример перехода от неуправляемого асинхронного исключения ThreadAbortException к управляемому HttpException с логгированием ситуации в журнал счетчиков производительности.

Класс HttpApplication HttpApplication.cs

internal Exception ExecuteStep(IExecutionStep step, ref bool completedSynchronously) { Exception error = null; try { try { // ... } catch (Exception e) { error = e; // ... // This might force ThreadAbortException to be thrown // automatically, because we consumed an exception that was // hiding ThreadAbortException behind it if (e is ThreadAbortException && ((Thread.CurrentThread.ThreadState & ThreadState.AbortRequested) == 0)) { // Response.End from a COM+ component that re-throws ThreadAbortException // It is not a real ThreadAbort // VSWhidbey 178556 error = null; _stepManager.CompleteRequest(); } } catch { // ignore non-Exception objects that could be thrown } } catch (ThreadAbortException e) { // ThreadAbortException could be masked as another one // the try-catch above consumes all exceptions, only // ThreadAbortException can filter up here because it gets // auto rethrown if no other exception is thrown on catch if (e.ExceptionState != null && e.ExceptionState is CancelModuleException) { // one of ours (Response.End or timeout) -- cancel abort // ... Thread.ResetAbort(); } }
}

NET Framework. Здесь описывается очень интересный случай: когда мы ждем не настоящий ThreadAbort (мне вот в некотором смысле жалко команду CLR и . Обработка ситуации идет в два этапа: внутренним обработчиком мы ловим ThreadAbortException но при этом проверяем наш поток на флаг реальной прерываемости. Сколько не стандартных ситуаций им приходится обрабатывать, подумать страшно). Такие ситуации мы должны обработать соответствующим образом: спокойно поймать исключение и обработать его. Если поток не помечен как прерывающийся, то на самом деле это не настоящий ThreadAbortException. Если он удовлетворяет необходимым условиям, он также будет обработан путем очистки флага ThreadState. Если же мы получаем настоящий ThreadAbort, то он уйдет во внешний catch поскольку ThreadAbortException должен войти во все подходящие обработчики. ResetAbort(). AbortRequested методом Thread.

Abort(), то все примеры кода в . Если говорить про примеры самого вызова Thread. Для наглядности приведу только один: NET Framework написаны так что могут быть переписаны без его использования.

Класс QueuePathDialog QueuePathDialog.cs

protected override void OnHandleCreated(EventArgs e)
{ if (!populateThreadRan) { populateThreadRan = true; populateThread = new Thread(new ThreadStart(this.PopulateThread)); populateThread.Start(); } base.OnHandleCreated(e);
} protected override void OnFormClosing(FormClosingEventArgs e)
{ this.closed = true; if (populateThread != null) { populateThread.Abort(); } base.OnFormClosing(e);
} private void PopulateThread()
{ try { IEnumerator messageQueues = MessageQueue.GetMessageQueueEnumerator(); bool locate = true; while (locate) { // ... this.BeginInvoke(new FinishPopulateDelegate(this.OnPopulateTreeview), new object[] { queues }); } } catch { if (!this.closed) this.BeginInvoke(new ShowErrorDelegate(this.OnShowError), null); } if (!this.closed) this.BeginInvoke(new SelectQueueDelegate(this.OnSelectQueue), new object[] { this.selectedQueue, 0 });
}

TheradAbortException во время AppDomain.Unload

Для этого искусственно создадим не вполне нормальную ситуацию, но достаточно интересную с точки зрения исполнения кода. Попробуем отгрузить AppDomain во время исполнения кода, который в него загружен. В основном мы создаем новый домен, в котором запускаем новый поток. В данном примере у нас два потока: один создан для того чтобы получить в нем ThreadAbortException, а другой — основной. Чтобы методы дочернего домена осталиь бы только в Stack Trace. Задача этого потока — уйти в основной домен. После этого основной домен отгружает дочерний:

class Program : MarshalByRefObject
{ static void Main() { try { var domain = ApplicationLogger.Go(new Program()); Thread.Sleep(300); AppDomain.Unload(domain); } catch (ThreadAbortException exception) { Console.WriteLine("Main AppDomain aborted too, {0}", exception.Message); } } public void InsideMainAppDomain() { try { Console.WriteLine($"InsideMainAppDomain() called inside {AppDomain.CurrentDomain.FriendlyName} domain"); // AppDomain.Unload will be called while this Sleep Thread.Sleep(-1); } catch (ThreadAbortException exception) { Console.WriteLine("Subdomain aborted, {0}", exception.Message); // This sleep to allow user to see console contents Thread.Sleep(-1); } } public class ApplicationLogger : MarshalByRefObject { private void StartThread(Program pro) { var thread = new Thread(() => { pro.InsideMainAppDomain(); }); thread.Start(); } public static AppDomain Go(Program pro) { var dom = AppDomain.CreateDomain("ApplicationLogger", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, }); var proxy = (ApplicationLogger)dom.CreateInstanceAndUnwrap(typeof(ApplicationLogger).Assembly.FullName, typeof(ApplicationLogger).FullName); proxy.StartThread(pro); return dom; } } }

Код выгрузки домена помимо самой выгрузки ищет вызванные в этом домене методы, которые еще не завершили работу в том числе в глубине стека вызова методов и вызывает ThreadAbortException в этих потоках. Происходит крайне интересная вещь. Т.е. Это важно, хоть и не очевидно: если домен отгружен, нельзя осуществить возврат в метод, из которого был вызван метод основного домена, но который находится в отгружаемом. Unload может выбрасывать потоки, исполняющие в данный момент код из других доменов. другими словами AppDomain. Abort в таком случае не получится: исполнять код выгруженного домена вы не сможете, а значит Thread. Прервать Thread. ResetAbort. Abort завершит свое дело, даже если вы вызовите Thread.

Выводы по ThreadAbortException

  • Это — асинхронное исключение, а значит оно может возникнуть в любой точке вашего кода (но, стоит отметить, что для этого надо постараться);
  • Обычно код обрабатывает только те ошибки, которые ждет: нет доступа к файлу, ошибка парсинга строки и прочие подобные. Наличие асинхронного (в плане возникновения в любом месте кода) исключения создает ситуацию, когда try-catch могут быть не обработаны: вы же не можете быть готовым к ThreadAbort в любом месте приложения. И получается, что это исключение в любом случае породит утечки;
  • Обрыв потока может также происходить из-за выгрузки какого-либо домена. Если в Stack Trace потока существуют вызовы методов отгружаемого домена, поток получит ThreadAbortException без возможности ResetAbort;
  • В общем случае не должно возникать ситуаций, когда вам нужно вызвать Thread.Abort(), поскольку результат практически всегда — не предсказуем.
  • CoreCLR более не содержит ручной вызов Thread.Abort(): он просто удален из класса. Но это не означает что его нет возможности получить.

ExecutionEngineException

В комментарии к этому исключению стоит атрибут Obsolete с комментарием:

The runtime no longer raises this exception so this type is obsolete This type previously indicated an unspecified fatal error in the runtime.

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

void Main()
{ var counter = 0; AppDomain.CurrentDomain.FirstChanceException += (_, args) => { Console.WriteLine(args.Exception.Message); if(++counter == 1) { throw new ArgumentOutOfRangeException(); } }; throw new Exception("Hello!");
}

Возможно это показалось страным разработчикам ядра и они посчитали что корректнее выбросить ExecutionEngineException. Результатом данного кода будет ExecutionEngineException, хотя ожидаемое мной поведение Unhandled Exception ArgumentOutOfRangeException из инструкции throw new Exception("Hello!").

Если вы напутаете с размерами типов, передадите больше чем надо, чем испортите, например, стек потока, ждите ExecutionEngineException. Второй вполне простой путь получить ExecutionEngineException — это не корректно настроить маршаллинг в мир unsafe. Не понятным, как его восстанавливать. И это будет логичный, правильный результат: ведь в данной ситуации CLR вошла в состояние, которое она нашла не консистентным. И как результат, ExecutionEngineException.

Каковы могут быть причины его возникновения? Отдельно стоит поговорить про диагностику ExecutionEngineException. Если исключение вдруг возникло в вашем коде, необходимо ответить на несколько вопросов:

  • Используются ли в вашем приложении unsafe библиотеки? Вами или же может сторонними библиотеками. Попробуйте для начала выяснить, где конкретно приложение получает данную ошибку. Если код уходит в unsafe мир и получает ExecutionEngineException там, тогда необходимо тщательно проверить сходимость сигнатур методов: в нашем коде и в импортируемом. Помните, что если импортируются модули написанные на Delphi и прочих вариациях языка Pascal, то аргументы должны идти в обратном порядке (настройка производится в DllImport: CallingConvention.StdCall);
  • Подписаны ли вы на FirstChanceException? Возможно его код вызвал исключение. В таком случае просто оберните обработчик в try-catch(Exception) и обязательно сохраните в журнал ошибок происходящее;
  • Может быть ваше приложение частично собрано под одну платформу, а частично — под другую. Попробуйте очистить кэш nuget пакетов, полностью пересобрать приложение с нуля: с очищенными вручную папками obj/bin
  • Проблема иногда бывает в самом фреймворке. Например, в ранних версиях .NET Framework 4.0. В этом случае стоит протестировать отдельный участок кода, который вызывает ошибку — на более новой версии фреймворка;

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

Corrupted State Exceptions

NET Framework можно отнести в первую очередь Qt, Java и С++ Builder, нам был дан вектор на виртуализацию исполнения кода приложения от окружающей среды. После становления платформы и ее популяризации, после того как огромная масса програмистов начала мигрировать с C/C++ и MFC (Microsoft Foundation Classes) на более приятные мозгу среды разработки: сюда помимо . NET Framework начал занимать свою нишу. Со временем, удачно сверстанный архитектурно, . И если раньше мы в большинстве случаев были вынуждены работать с феерическим пластом компонентов, написанных на COM/ATL/ActiveX (вы помните перетаскивание на формочки иконок COM/ActiveX компонент в Borland C++ Builder?), то теперь всем нам стало сильно легче жить. С годами, окрепнув и набирая массу, можно начинать рассуждать не с точки зрения приспособленца, а с точки зрения силы. NET Framework. Ведь теперь соответствующие технологии достаточно редки чтобы волноваться и появилась возможность сделать их несколько неудобными чтобы от них начали отказываться полностью, переходя на современный и ладный . А потому можно сделать еще один шаг к закрытию песочницы: сделать её ещё более непробиваемой, сделать её ещё более managed. Старые технологии, которые не то что существовали 5-10 лет назад, а на самом деле существуют и здравствуют и ныне, кажется нам чем-то архаичным, забытым, "кривым" и допотопным.

Давайте разберемся, что это за исключительные ситуации, а саму историю еще раз проследим на одной из них — AccessViolationException: И один из этих шагов — введение понятия Corrupted State Exceptions, что по сути ставит ряд исключительных ситуаций вне закона.

Файл util.cpp util.cpp

BOOL IsProcessCorruptedStateException(DWORD dwExceptionCode, BOOL fCheckForSO /*=TRUE*/)
{ // ... // If we have been asked not to include SO in the CSE check // and the code represent SO, then exit now. if ((fCheckForSO == FALSE) && (dwExceptionCode == STATUS_STACK_OVERFLOW)) { return fIsCorruptedStateException; } switch(dwExceptionCode) { case STATUS_ACCESS_VIOLATION: case STATUS_STACK_OVERFLOW: case EXCEPTION_ILLEGAL_INSTRUCTION: case EXCEPTION_IN_PAGE_ERROR: case EXCEPTION_INVALID_DISPOSITION: case EXCEPTION_NONCONTINUABLE_EXCEPTION: case EXCEPTION_PRIV_INSTRUCTION: case STATUS_UNWIND_CONSOLIDATE: fIsCorruptedStateException = TRUE; break; } return fIsCorruptedStateException;
}

Рассмотрим описания наших исключительных ситуаций:

Код ошибки

Описание

STATUS_ACCESS_VIOLATION

Достаточно частая ошибка попытки работы с участком памяти, на который нет прав. Память с точки зрения процесса линейная, но работать можно далеко не со всем диапазоном: только с теми «островками», которые были выделены операционной системой, а также с теми, на которые хватает прав (например, есть диапазоны, которыми владеет только операционная система или же можно только исполнять код но не читать как данные)

STATUS_STACK_OVERFLOW

Эта ошибка известна всем: ошибка нехватки памяти в стеке потока под вызов очередного метода

EXCEPTION_ILLEGAL_INSTRUCTION

Очередной код, считанный процессором из тела метода не был распознан как инструкция

EXCEPTION_IN_PAGE_ERROR

Поток предпринял попытку работы со страницей памяти, которой не существует

EXCEPTION_INVALID_DISPOSITION

Механизм обработки исключений вернул не правильный обработчик. Такое исключение никогда не должно возникать в программах, написанных на высокоуровневых языках (например, С++)

EXCEPTION_NONCONTINUABLE_EXCEPTION

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

EXCEPTION_PRIV_INSTRUCTION

Попытка выполнить привилегированную инструкцию процессора

STATUS_UNWIND_CONSOLIDATE

Исключение, относящееся к размотке стека и не являющееся предметом наших обсуждений

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

AccessViolationException

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

  • Если StackTrace уходит в недра CLR, вам сильно не повезло: это скорее всего ошибка ядра. Однако этот случай почти никогда не срабатывает. Из вариантов обхода — либо действовать как-то иначе либо обновить версию ядра если возможно;
  • Если же StackTrace уходит в unsafe код некоторой библиотеки, тут доступны такие варианты: либо вы напутали с настройкой маршаллинга либо же в unsafe библиотеке закралась серьезная ошибка. Тщательно проверьте аргументы метода: возможно аргументы нативного метода имеют другую разрядность или же другой порядок или попросту размер. Проверьте что структуры передаются там где надо — по ссылке, а там, где надо — по значению

В противном случае оно перехвачено никак не будет и вы получите упавшее приложение. Чтобы перехватить такое исключение на данный момент необходимо показать JIT компилятору что это реально необходимо. А в этом случае что может пойти не так не известно никому: вы не можете знать каким образом было нарушено состояние приложения в прошлый раз. Однако, конечно же, его стоит перехватывать только тогда, когда вы понимаете что вы сможете его правильно обработать: его наличие может свидетельствовать о произошедшей утечке памяти если она была выделена unsafe методом между точкой его вызова и точкой выброса AccessViolationException и тогда хоть приложение и не будет "завалено", но его работа возможно станет не корректной: ведь перехватив поломку вызова метода вы наверняка попробуете вызвать этот метод еще раз, в будущем. NET Framework: Однако, если желание перехватить такое исключение у вас сохранилось, прошу посмотреть на таблицу возможности перехвата этого исключения в различных версиях .

Версия .NET Framework

AccessViolationExeception

1.0

NullReferenceException

2.0, 3.5

AccessViolation перехватить можно

4.0+

AccessViolation перехватить можно, но необходима настройка

.NET Core

AccessViolation перехватить нельзя

другими словами, если вам попалоось очень старое приложение на . Т.е. 0, покажите его мне вы получите NRE, что будет в некоторой степени обманом: вы отдали указатель со значением больше нуля, а получили NullReferenceException. NET Framework 1. NET — тут вполне подходит. Однако, на мой взгляд, такое поведение обосновано: находясь в мире управляемого кода вам меньше всего должно хотеться изучать типы ошибок неуправляемого кода и NRE — что по сути и есть "плохой указатель на объект" в мире . В реальных ситуациях пользователям решительно не хватало этого типа исключений и потому — достаточно скоро — его ввели в версии 2. Однако, мир был бы прекрасен если бы все так было просто. Просуществовав несколько лет в перехватываемом варианте, исключение перестало быть перехватываемым, однако появилась специальная настройка, которая позволяет включить перехват. 0. Посудите сами: Такой порядок выбора в команде CLR в целом на каждом этапе выглядит достаточно обоснованным.

  • 1.0 Ошибка промаха мимо выделенных участков памяти должна быть именно исключительной ситуацией потому как если приложение работает с каким-либо адресом, оно его откуда-то получило. В managed мире этим местом является оператор new. В unmanaged мире — в целом любой участок кода может выступать точкой для возникновения такой ошибки. И хотя с точки зрения философии смысл обоих исключений диаметрально противоположен (NRE — работа с не проинициализированным указателем, AVE — работа с некорректно проинициализированным указателем), с точки зрения идеологии .NET некорректно проинициализированных указателей быть не может. Оба случая можно свести к одному и придать философский смысл: не корректно заданный указатель. А потому давайте так и сделаем: в обоих случаях будем выбрасывать NullReferenceException.
  • 2.0 На ранних этапах существования .NET Framework оказалось что кода, который наследуется через COM библиотеки больше собственного: существует огромная кодовая база коммерческих компонент для взаимодействия с сетью, UI, БД и прочими подсистемами. А значит, вопрос получения именно AccessViolationException все-таки стоит: неверная диагностика проблем может сделать процесс поимки проблемы более дорогим. В .NET Framework введено исключение AccessViolationException.
  • 4.0 .NET Framework укоренился, потеснив традиционную разработку на низкоуровневых языках программирования. Резко сокращено количество COM компонент: практически все основные задачи уже решаются в рамках самого фреймворка, а работа в unsafe кодом начинает считаться чем-то странным, неправильным. В этих условиях можно вернуться к идеологии, введенной в фреймворк с самого начала: .NET — он только для .NET. Unsafe код — это не норма, а вынужденное состояние, а потому идеологичность наличия перехвата AccessViolationException идет вразрез с идеологией понятия фреймворк — как платформа (т.е. имитация полной песочницы со своими законами). Однако мы все еще находимся в реалиях платформы, на которой работаем и во многих ситуациях перехватывать это исключение все еще необходимо: вводим специальный режим перехвата: только если введена соответствующая конфигурация;
  • .NET Core Наконец, сбылась мечта команды CLR: .NET более не предполагает законности работы с unsafe кодом, а потому существование AccessViolationException теперь вне закона даже на уровне конфигурации. .NET вырос настолько чтобы самостоятельно устанавливать правила. Теперь существование этого исключения в приложении приведет его к гибели, а потому любой unsafe код (т.е. сам CLR) обязан быть безопасным с точки зрения этого исключения. Если оно появляется в unsafe библиотеке, с ней просто не будут работать, а значит разработчики сторонних компонент на unsafe языках будут более аккуратными и обрабатывать его — у себя.

NET Framework как платформы: от неуверенного подчинения внешним правилам до самостоятельного установления правил самой платформой. Вот так, на примере одного исключения можно проследить историю становления .

0+. После всего сказанного осталось раскрыть последнюю тему: как включить обработку данного исключения в 4. Итак, чтобы включить обработку исключения данного типа в конкретном методе, необходимо:

  • Добавить в секцию configuration/runtime следующий код: <legacyCorruptedStateExceptionsPolicy enabled="true|false"/>
  • Для каждого метода, где необходимо обработать AccessViolationException, надо добавить два атрибута: HandleProcessCorruptedStateExceptions и SecurityCritical. Эти атрибуты позволяют включить обработку Corrupted State Exceptions, для конкретных методов, а не для всех вообще. Эта схема очень правильная, поскольку вы должны быть точно уверены что хотите их обрабатывать и знать, где: иногда более правильный вариант — просто завалить приложение на бок.

Для примера включения обработчика CSE и их примитивной обработки рассмотрим следующий код:

[HandleProcessCorruptedStateExceptions, SecurityCritical]
public bool TryCallNativeApi()
{ try { // Запуск метода, который может выбросить AccessViolationException } catch (Exception e) { // Журналирование, выход System.Console.WriteLine(e.Message); return false; } return true;
}

StackOverflowException

Она возникает тогда, когда по сути в массиве, выделенном под стек кончается память. Последний тип исключений, о котором стоит поговорить — это ошибка переполнения стека. Само строение стека мы подробно обсудили в соответствующей главе (Стек потока), а здесь без сильного углубления остановимся на самой ошибке.

По сути этот диапазон — ловушка и не занимает никакой физической памяти. Итак, когда наступает нехватка памяти под стек потока (либо уперлись в следующий занятый участок памяти и нет возможности выделить следующую страницу виртуальной памяти) или же поток уперся в разрешенный диапазон памяти, происходит попытка доступа к адресному пространству, которое называется Guard page. В случае достижения максимально-разрешенных значений операционная система вместо выделения нового участка генерирует исключительную ситуацию с кодом STATUS_STACK_OVERFLOW, которая будучи проброшенной через Structured Exception Handling механизм в . Вместо реального чтения или записи процессор вызывает специализированное прерывание, которое должно запросить у операционной системы новый участок памяти под рост стека потока. NET обрушивает текущий поток исполнения как более не корректный.

Т.е. Прошу заметить что хоть данное исключение и является Corrupted State Exception, перехватить его при помощи HandleProcessCorruptedStateExceptions не представляется возможным. следующий код не отработает:

// Main.cs
[HandleProcessCorruptedStateExceptions, SecurityCritical]
static void Main()
{ try { Recursive(); } catch (Exception exception) { Console.WriteLine("Catched Stack Overflow!"); }
} static void Recursive()
{ Recursive();
} // app.config:
<configuration> <runtime> <legacyCorruptedStateExceptionsPolicy enabled="true"/> </runtime>
</configuration>

Тут может возникнуть желание исправить ситуацию, перехватив выброс исключения. А не представляется возможным потому что переполнение стека может быть вызвано двумя путями: первый — это намеренный вызов рекурсивного метода, который не слишком аккуратен чтобы контролировать собственную глубину. И вторая — случайность — получение StackOverflowException при вполне себе обычном вызове. Однако, если подумать, мы тем самым легализуем данную ситуацию, позволяя ей случиться вновь, что выглядит скорее не дальновидным чем случаем проявления заботы. В данном примере перехватывать исключение выглядит как что-то совсем дикое: приложение работало штатно, все шло хорошо как вдруг легальный вызов метода при корректно работающих алгоритмах вызвал выброс исключения с дальнейшей разверткой стека до ожидающего такого поведения участка кода. Просто в данный момент глубина стека вызовов оказалась слишком критичной. Как по мне, это полный абсурд. Хм… Еще раз: мы ждем, что на следующем участке ничего не отработает потому что в стеке кончится память.

Ссылка на всю книгу

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть