1. CLR приостанавливает все потоки в процессе, которые когда-либо выполняли управляемый код.

2. CLR проверяет все стеки на наличие потоков, которые в текущий момент выполняют код выгружаемого домена или могут рано или поздно вернуться к выполнению такого кода. CLR вынуждает все потоки, в стеке которых находится выгружаемый домен, сгенерировать исключение ThreadAbortException (при этом выполнение потока возобновляется). В результате потоки переходят к выполнению блоков finally, то есть корректно завершают свою работу. При отсутствии кода, перехватывающего исключение ThreadAbortException, оно переходит в разряд необработанных и «проглатывается» CLR; поток завершается, но процессу разрешается продолжить работу. Такое поведение отличается от стандартного, потому что в любых других ситуациях при возникновении необработанного исключения CLR уничтожает процесс.

ВНИМАНИЕ

CLR не уничтожит немедленно поток, который выполняет код блока finally или catch, конструктора класса, критической области или неуправляемый код. Уничтожение таких потоков сделало бы невозможным выполнение кода очистки, восстановления после ошибок, инициализации типа, критического кода или любого кода, который CLR не знает как обрабатывать. Это стало бы причиной непредсказуемого поведения приложений и появлению дыр в системе безопасности. Уничтожаемому потоку разрешается закончить выполнение таких блоков, и только после этого CLR вынуждает его сгенерировать исключение ThreadAbortException.

3. После выгрузки из домена всех потоков, обнаруженных на втором шаге, CLR проходит по куче и устанавливает флаг для каждого объекта-представителя, который ссылается на объект, созданный в выгружаемом домене. Так объекты- представители «узнают», что реальный объект, на который они ссылаются, уничтожен. В результате при попытке вызвать метод «неправильного» объекта- представителя появляется исключение AppDomainUnloadedException.

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

5. CLR возобновляет работу всех оставшихся потоков. Поток, вызвавший метод AppDomain. Unload, продолжает работу; вызовы AppDomain .Unload выполняются синхронно.

В моем приложении всю работу выполняет один поток. Всякий раз, когда код приложения вызывает метод AppDomain.Unload, в выгружаемом домене не оказывается потоков, поэтому CLR не приходится генерировать исключение ThreadAbortException (о нем мы поговорим чуть позже).

Кстати, при вызове потоком метода AppDomain.Unload CLR ждет 10 секунд, чтобы потоки выгружаемого домена могли его покинуть. Если после этого поток, вызвавший метод AppDomain .Unload, не возвращает управление, он генерирует исключение CannotUnloadAppDomainException, и домен может быть (а может и не быть) выгруженным в будущем.

ПРИМЕЧАНИЕ

Если поток, вызвавший AppDomain.Unload, находится в выгружаемом домене, CLR создает другой поток, который пытается выгрузить домен. Первый поток принудительно генерирует исключение ThreadAbortException и выполняет раскрутку — поиск и очистку всех операций, начатых ниже по стеку вызовов. Е1овый поток дожидается выгрузки домена, а затем завершается. В случае сбоя выгрузки новый поток попробует обработать исключение CannotUnloadAppDomainException, но так как нет кода, выполняемого этим новым потоком, перехватить это исключение нельзя.

Мониторинг доменов

Хост-приложение умеет отслеживать потребляемые доменом ресурсы. Некоторые хосты на основе этой информации принудительно выгружают домен, если вдруг потребление им памяти или ресурсов процессора выходит за разумные с точки зрения хоста пределы. Мониторинг позволяет также сравнить потребление ресурсов различными алгоритмами и сделать выбор. Но так как эта операция влияет на производительность, она инициируется вручную путем присвоения статическому свойству MonitoringEnabled класса AppDomain значения true. При этом включается мониторинг всех доменов. Выключить его уже нельзя; попытка присвоить свойству MonitoringEnabled значение false приведет к исключению ArgumentException.

При включенном мониторинге код может использовать четыре предназначенных только для чтения свойства класса AppDomain:

□ MonitoringSurvivedProcessMemorySize. Это статическое свойство типа Int64 возвращает число байтов, используемых в данный момент всеми доменами под управлением текущего экземпляра CLR. Значение верно с момента последней уборки мусора.

□ MonitoringTotalAllocatedMemorySize. Это свойство экземпляра типа Int64 возвращает количество байтов, выделенных определенным доменом. Значение верно с момента последней уборки мусора.

□ MonitoringSurvivedMemorySize. Это свойство экземпляра типа Int64 возвращает количество байтов, которые в настоящее время используются определенным доменом. Значение верно с момента последней уборки мусора.

□ MonitoringTotalProcessorTime. Это свойство экземпляра типа TimeSpan возвращает процессорное время, использованное определенным доменом.

Следующий класс демонстрирует, как при помощи трех из этих свойств узнать об изменении состояния домена за некий промежуток времени:

private sealed class AppDomainMonitorDelta : IDisposable { private AppDomain m_appDomain; private TimeSpan m_thisADCpu; private Int64 m_thisADMemoryInllse; private Int64 m_thisADMemoryAllocated;

static AppDomainMonitorDelta() {

// Проверяем, что включен режим мониторинга домена AppDomain.MonitoringlsEnabled = true;

}

public AppDomainMonitorDelta(AppDomain ad) { mappDomain = ad ?? AppDomain.CurrentDomain; mthisADCpu = m_appDomain.MonitoringTotalProcessorTime; mthisADMemorylnUse = m_appDomain.MonitoringSurvivedMemorySize;

m_thisADMemoryAllocated =

m_appDomain.MonitoringTotalAllocatedMemorySize;

}

public void DisposeQ {

GC.CollectQ;

Console.WriteLine("FriendlyName={0}, CPU={l}ms", m_appDomain.FriendlyName,

(m_appDomain.MonitorIngTotalProcessorTime - m_thisADCpu).TotalMllllseconds);

Console.WriteLine(

" Allocated {0:N0} bytes of which {1:N0} survived GCs", mappDomain.MonitoringTotalAllocatedMemorySize - m_thisADMemoryAllocated,

mappDomain.MonitoringSurvivedMemorySize - m_thisADMemoryInUse);

}

}

А это — пример применения класса AppDomainMonitorDelta:

private static void AppDomainResourceMonitoring() { using (new AppDomainMonitorDelta(null)) {

// Выделение около 10 миллионов байтов,

// которые переживут сборку мусора var list = new List<Object>();

for (Int32 x = 0; x < 1000; x++) list.Add(new Byte[10000]);

// Выделение около 20 миллионов байтов,

// которые НЕ переживут уборку мусора

for (Int32 х = в; х < 2000; х++) new Byte[10000].GetType();

// Прокрутка процессора около 5 секунд Int64 stop = Environment.TickCount + 5000; while (Environment.TickCount < stop) ;

}

При выполнении этого кода выводится следующий результат:

FriendlyName=03-Ch22-l-AppDomains.exe, CPU=5031.25ms Allocated 30,159,496 bytes of which 10,085,080 survived GCs

Уведомление о первом управляемом исключении домена

С каждым доменом связан набор методов обратного вызова, активизирующихся, когда CLR начинает искать внутри домена блоки catch. Эти методы могут выполнять записи в журнал, кроме того, хост в состоянии использовать этот механизм для отслеживания сгенерированных внутри домена исключений. Обратные вызовы не могут обработать исключение или «поглотить» его; они просто получают уведомление. Для регистрации такого метода обратного вызова достаточно добавить делегат к экземпляру события FinstChanceException класса AppDomain.

Среда CLR в данном случае работает так: при первом появлении исключения она задействует любой из методов обратного вызова FinstChanceException, зарегистрированный в домене, ставшем источником исключения. Затем CLR ищет в стеке этого домена блоки catch. Если какой-то из этих блоков обрабатывает исключение, выполнение программы возвращается в обычный режим. Если же такой блок отсутствует, CLR идет наверх стека вызывающего домена и снова генерирует то же самое исключение (после его сериализации и десериализации). Однако это исключение начинает восприниматься, как новое, и CLR задействует методы обратного вызова FinstChanceException, зарегистрированные на данный момент в текущем домене. Процесс продолжается, пока не будет достигнут верх стека потока. Если в результате исключение так и не удастся обработать, CLR завершает процесс.

Использование хостами доменов приложений

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

Исполняемые приложения

Консольные приложения, приложение NT Service, приложения Windows Forms и приложения Windows Presentation Foundation (WPF) являются саморазмещаю- щимися (self-hosted) и снабжены управляемыми ЕХЕ-файлами. Инициализировав процесс при помощи такого файла, Windows загружает оболочку совместимости, которая исследует информацию в заголовке CLR, содержащуюся в сборке приложения (ЕХЕ-файле). Эта информация указывает версию CLR, которая использовалась при сборке и тестировании приложения. Именно с ее помощью оболочка совместимости определяет, какую версию CLR следует загрузить в процесс. Загрузив и инициализировав среду, оболочка совместимости снова исследует заголовок ее сборки, чтобы определить, какой метод является точкой входа приложения (Main). CLR вызывает этот метод, и приложение начинает работу.

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

возвращения управления методом Main Windows-процесс завершает свою работу (ликвидируя основной и все прочие домены).

ПРИМЕЧАНИЕ

Кстати, для завершения Windows-процесса со всеми его доменами можно воспользоваться статическим методом Exit класса System.Environment. Это самый корректный способ завершения процесса, так какданный метод сначала вызывает методы фина- лизации всех объектов в управляемой куче, а затем освобождает все неуправляемые COM-объекты в CLR. После этого вызывается Шт32-функция ExitProcess.

Приложение может заставить CLR создать дополнительные домены в адресном пространстве процесса. Собственно, именно это и происходит в моем приложении, код которого представлен в начале этой главы.

Полнофункциональные интернет-приложения Silverlight

Версия CLR для разработанной в Microsoft динамической технологии Silverlight отличается от версии обычной платформы .NET Framework для настольных компьютеров. После установки среды Silverlight перемещение на сайт, который использует эту среду, заставляет Silverlight CLR (CoreClr.dll) загрузить браузер (причем это может быть вовсе не Internet Explorer — вы можете вообще не использовать Windows). Каждый элемент управления Silverlight на странице работает в собственном домене. Когда пользователь закрывает вкладку или переходит на другой сайт, то домены всех неиспользуемых элементов управления Silverlight выгружаются. Код Silverlight в домене запускается в песочнице (sandbox) с ограниченной защитой и не может причинить какого-либо вреда пользователю или машине.

MicrosoftASP.NET и веб-службы XML

ASRNET — это библиотека ISAPI (реализованная в файле ASPNet_ISAPI.dll). При первом запросе клиентом URL-адреса, обрабатываемого этой библиотекой, ASR NET загружает CLR. Когда клиент обращается с запросом к веб-приложению, ASP.NET определяет, были ли уже такие запросы. Если данный запрос является первым, CLR получает команду создать для данного веб-приложения новый домен (каждое приложение идентифицируется собственным виртуальным корневым каталогом). Далее ASRNET заставляет CLR загрузить в новый домен сборку, предоставляющую нужный тип, в новый домен приложений, создает экземпляр этого типа и начинает вызывать его методы для исполнения запроса клиента. При наличии ссылок на другие типы CLR загружает в домен веб-приложения дополнительные сборки.

При поступлении запроса к уже работающему веб-приложению ASP.NET вместо нового домена создает новый экземпляр типа веб-приложения в существующем и начинает вызывать его методы. При этом вызываемые методы оказываются уже преобразованными JIT-компилятором в машинный код, поэтому следующие клиентские запросы обрабатываются намного быстрее.

Если клиент обращается с запросом к другому веб-приложению, ASP.NET заставляет CLR создать новый домен. Обычно он создается в том же рабочем процессе, в котором работают другие домены приложений. Это значит, что в одном Windows-процессе может работать несколько веб-приложений, что повышает производительность системы. В этом случае сборки для разных веб-приложений загружаются в собственный домен каждого из них — это необходимо для изоляции кода и объектов веб-приложения от других веб-приложений.

Замечательная особенность ASP.NET — возможность изменять код веб-сайта без остановки веб-сервера. Когда файл на жестком диске сайта меняется, ASP.NET обнаруживает это, выгружает домен, содержащий старую версию (после завершения текущего запроса), а затем создает новый домен, загружая в него новые версии файлов. При этом ASP.NET использует особую функцию доменов, называемую теневым копированием (shadow copying).

Microsoft SQL Server

Microsoft SQL Server относится к неуправляемым приложениям, так как большая часть кода SQL-сервера написана на неуправляемом языке C++. SQL-сервер поддерживает создание хранимых процедур на управляемом коде. При первом получении запроса на выполнение хранимой процедуры на управляемом коде SQL-сервер загружает CLR. Хранимые процедуры выполняются в собственном защищенном домене, что не позволяет им нарушить работу сервера базы данных.

Это просто замечательная функциональность! Ведь разработчики могут выбирать язык программирования для создания хранимых процедур. Хранимые процедуры могут использовать в своем коде объекты с сильной типизацией. Кроме того, код компилируется JIT-компилятором в машинный код и выполняется, а не интерпретируется. Также разработчикам таких процедур доступны все типы, определенные в библиотеке FCL или любой другой сборке. В результате разработка хранимых процедур значительно упрощается, а приложения работают намного быстрее. Что еще нужно программисту для счастья?

Будущее и мечты

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

Нетривиальное управление хостингом

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

Применение управляемого кода

Класс System. AppDomainManager позволяет хосту менять заданное по умолчанию поведение CLR при помощи управляемого кода. Конечно, это упрощает реализацию хоста. Вам требуется только определить собственный класс, производный от System. AppDomainManager, переопределив все необходимые виртуальные методы. Далее этот класс надо скомпоновать в отдельную сборку и установить ее в глобальный кэш сборок (GAC), предоставив ей тем самым полное доверие.

Затем нужно заставить CLR использовать свой класс, производный от AppDomainManager. Это лучик; всего сделать, создав объект AppDomainSetup и инициализировав его свойства AppDomainManagerAssembly и AppDomainManagerType типа String. Свойству AppDomainManagerAssembly присваивается строка со строгим именем сборки, определяющей ваш класс, производный от класса AppDomainManager. Свойству же AppDomainManagerType присваивается полное имя этого класса. Кроме того, свойству AppDomainManager с помощью элементов appDomainManagerAssembly и appDomainManagerType можно присвоить конфигурационный XML-файл вашего приложения. Также собственный хост может отправить запрос к интерфейсу ICLRControl и вызвать его свойство SetAppDomainManagerType, передав туда идентификатор установленной в GAC сборки и имя класса, производного от AppDomainManager[29].

Теперь поговорим о функциях класса, производного от AppDomainManager. Он позволяет хосту сохранить контроль, даже когда надстройка пытается создать собственный домен. При этом объект, производный от AppDomainManager, может редактировать параметры защиты и конфигурирования. Кроме того, он в состоянии помешать созданию нового домена или вернуть ссылку на уже существующий. Когда новый домен уже создан, CLR формирует в нем новый объект, производный от AppDomainManager. Он также может редактировать параметры конфигурирования, контекст выполнения между потоками и разрешения, предоставленные сборке.

Разработка надежных хост-приложений

Хост может указать CLR, какие действия следует предпринимать при сбое в управляемом коде. Вот несколько примеров (от наименее до наиболее серьезного):

□ CLR может прервать поток, если тот выполняется слишком долго или долго не возвращает управление (детали см. в следующем разделе).

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

□ CLR может отключиться. При этом прекращается выполнение любого кода в процессе, но неуправляемому коду работать разрешено.

□ CLR может выйти из Windows-процесса. При этом сначала закрываются все потоки и выгружаются все домены — выполняются операции очистки, после чего процесс закрывается.

CLR может завершить поток или домен как корректно, так и принудительно. Корректное завершение предусматривает выполнение кода очистки. Иначе говоря, выполняется код в блоках finally и вызываются методы финализации объектов. При принудительном завершении код очистки игнорируется. При корректном завершении, в отличие от принудительного, не удастся закрыть поток в блоках catch и finally. Поток, выполняющий неуправляемый код или находящийся в критической области (Critical Execution Region, CER), завершить вообще нельзя.

Хост может установить так называемую политику расширения (escalation policy), определив тем самым поведение CLR при сбоях управляемого кода. Например, SQL- сервер определяет, что должна делать среда CLR при появлении необработанного исключения во время выполнения управляемого кода. Когда в потоке возникает необработанное исключение, CLR сначала пытается корректно завершить поток. Если поток не закрывается за определенное время, CLR пытается перейти от корректного к принудительному завершению потока.

В большинстве случаев происходит именно так. Однако для потоков из критической области (critical region) действует другая политика. Поток, находящийся в критической области, блокируется в рамках синхронизации потоков, причем разблокировать его должен тот же самый поток, например, тот, что вызвал метод Monitor. Enter, метод WaitOne типа Mutex или один из методов AcquireReaderLock или AcquireWriterLockTHna ReaderWriterLock[30]. Ожидание методов AutoResetEvent, ManualResetEvent или Semaphore не указывает на пребывание потока в критической области, потому что другой поток может освободить этот объект синхронизации. Когда поток находится в критической области, CLR полагает, что он работает с данными, совместно используемыми несколькими потоками того же домена. Это и есть наиболее вероятная причина блокирования потока. При работе с общими данными простое завершение потока является неудачным решением, потому что к данным могут обращаться другие потоки. Данные же уже повреждены. В итоге мы получаем непредсказуемое поведение домена и бреши в системе безопасности.

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

Возвращение потока в хост

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

Но что если код хранимой процедуры войдет в бесконечный цикл? То есть сервер базы данных «отдает» один из своих потоков для выполнения кода хранимой процедуры, но поток не возвращает управление. Сервер оказывается в опасном положении, и его поведение становится непредсказуемым. Например, может сильно упасть производительность. Может, серверу стоит создать дополнительные потоки? Но на это вам потребуются дополнительные ресурсы (например, место в стеке), кроме того, и этим потокам ничто не может помешать застрять в бесконечном цикле.

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