Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Управление ресурсами в .NET
Подобный материал:
1   ...   17   18   19   20   21   22   23   24   ...   47

Управление ресурсами в .NET




Нужно разобраться в том, что такое сборщик мусора в .NET (NET Garbage Collector).


Благодаря GC следить за утечками памяти при программировании на C# больше не нужно. Однако остаются ресурсы типа дескрипторов файлов, соединения с базами данных, объектами GDI+, объектами СОМ и прочими системными объектами. Эти ресурсы нужно во время освобождать.


Алгоритм Mark and Compact сборщика мусора эффективно выявляет недействительные связи между объектами и полностью удаляет недостижимые сети из объектов. GC определяет, является ли объект достижимым, путем прохождения по дереву объектов от корневого объекта приложения, вместо того чтобы заставлять каждый объект отслеживать ссылки на него, как это делается в СОМ. Класс DataSet может послужить хорошим примером того, как этот алгоритм упрощает принятие решений о том, кто является владельцем объекта. DataSet представляет собой коллекцию DataTable. Каждый DataTable является коллекцией DataRow. Каждый DataRow — это коллекция Dataltem. Кроме того, каждый DataTable является коллекцией DataColumns. DataColumns определяют типы, ассоциированные с каждым столбцом данных. Имеются и другие ссылки из Dataltem на соответствующие столбцы. Каждый Dataltem содержит также ссылку на свой контейнер, DataRow. DataRow содержит обратные ссылки на DataTable, а все классы содержат обратные ссылки на DataSet.


Если нужны более сложные структуры, можно создать DataView, которые обеспечивают доступ к отфильтрованным последовательностям DataTable. Все они управляются посредством DataViewManager. Имеются связи между объектами, образующими DataSet. При этом освобождение памяти является обязанностью сборщика мусора. Поскольку разработчикам в среде .NET Framework не требуется явно освобождать объекты, запутанная инфраструктура объектных ссылок перестает быть проблемой. Не нужно принимать решений о надлежащем порядке освобождения образовавшейся паутины объектов — это работа GC. GC спроектирован так, чтобы упростить проблему определения факта, что та или иная совокупность объектов является «мусором». После того как приложению перестает быть нужен DataSet, все подчиненные ему объекты перестают быть доступными. Неважно, что имеются циклические ссылки на DataSet, DataTables и другие объекты в сети. Так как все эти объекты недоступны из приложения, они являются «мусором».


Для работы сборщика мусора организуется отдельный поток, занимающийся удалением неиспользуемой программой памяти. При каждом своем запуске он перестраивает управляемую «кучу» так, чтобы свободное пространство выделялось единым непрерывным блоком. На след. рис. показаны два состояния «кучи»: перед и после операции по сборке мусоpa. После каждой такой операции вся свободная память будет помещена в один непрерывный блок.



Сборщик мусора не только удаляет неиспользуемые объекты, но и перемещает в памяти остальные объекты, чтобы сделать используемую память компактной и довести до максимума свободное пространство.


Итак, управление памятью полностью является обязанностью сборщика мусора. Все прочие системные ресурсы находятся под вашей ответственностью. Можно гарантировать, что системные ресурсы будут освобождены, если в разрабатываемом классе будет определен завершитель (finalizer). Завершители вызываются системой перед удалением из памяти объекта, являющегося «мусором». Можно и необходимо использовать эти методы для освобождения любых неконтролируемых ресурсов, которыми владеет объект. Завершитель для объекта вызывается в какой-то момент после того, как объект становится «мусором», и перед тем, как система получит в свое распоряжение занимаемую им память. Такое недетерминированное завершение означает, что невозможно контролировать связь между временем, когда прекращается использование объекта, и временем, когда будет выполнена программа завершителя. Это большое изменение по сравнению с С++, и оно должно учитываться при разработке проектов. Программисты на С++ писали классы, в которых критичные ресурсы выделялись в конструкторах класса и освобождались в деструкторах:


// Good C++, bad C#:

class CriticalSection

{

public:

// Constructor acquires the system resource.

CriticalSection( )

{

EnterCriticalSection( );

}


// Destructor releases system resource.

~CriticalSection( )

{

ExitCriticalSection( );

}

};


// usage:

void Func( )

{

// The lifetime of s controls access to

// the system resource.

CriticalSection s;

// Do work.


//...


// compiler generates call to destructor.

// code exits critical section.

}

Эта общепринятая в С++ идиома гарантирует, что выделение ресурсов является процессом, защищенным от исключительных ситуаций. Однако этот процесс не работает в С# — точнее, он работает, но не так, как в С++. Детерминистическое освобождение ресурсов не является характерной чертой среды .NET и языка С#. Попытки принудительно встроить в язык С# идиому детерминистического завершения С++ не срабатывают. В С# завершитель (финализатор, т.е. метод Object.Finalize) в конце концов будет выполнен, но он не будет выполнен тогда, когда этого хочет пользователь. В приведенном примере программа выйдет из критической секции, но в С# это произойдет вовсе не тогда, когда будет выполнен выход из функции. Это произойдет в какой-то неизвестный момент времени. Вы не знаете и не можете знать, когда это случится.


Для реализации метода Finalize в C# необходимо использовать синтаксис деструктора.

Деструкторы используются для уничтожения экземпляров классов.


В структурах определение деструкторов невозможно. Они применяются только в классах.

Класс может иметь только один деструктор.

Деструкторы не могут наследоваться или перегружаться.

Деструкторы невозможно вызвать. Они запускаются автоматически.

Деструктор не принимает модификаторы и не имеет параметров.


Например, следующая инструкция является объявлением деструктора класса Car:

class Car

{

~Car() // destructor

{

// cleanup statements...

}

}


Деструктор неявным образом вызывает метод Finalize для базового класса объекта. Следовательно, предыдущий код деструктора неявным образом преобразуется в следующий код:

protected override void Finalize()

{

try

{

// Cleanup statements...

}

finally

{

base.Finalize();

}

}


Это означает, что метод Finalize вызывается рекурсивно для всех экземпляров цепочки наследования начиная с самого дальнего и заканчивая самым первым.


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




Воздействие завершителей на сборщик мусора. Объекты остаются в памяти

дольше, и для выполнения программы сборщика мусора необходимо порождать дополнительные потоки


Возможно, вы подумали, что объекты, которым требуется завершение, «живут» в памяти на один цикл GC дольше, чем необходимо. Но реальное положение дел было упрощено. Все происходит сложнее, причем это связано с другим используемым в GC проектным решением. Для оптимизации своей работы сборщик мусора .NET вводит поколения. Поколения помогают ему быстрее выявлять наиболее вероятных кандидатов на попадание в мусор. Любой объект, созданный после последней операции сборки мусора, получает номер поколения 0. Любой объект, оставшийся «в живых» после одной операции CG, становится объектом поколения 1. Любой объект, переживший две или более операций по сборке мусора, становится объектом второго поколения. Цель состоит в том, чтобы разделить локальные переменные и объекты, которые остаются на все время жизни приложения. Объекты нулевого поколения по большей части являются локальными переменными. Переменные экземпляра (member variable) и глобальные переменные быстро переходят в поколение 1 и, в конечном счете, становятся поколением 2.


Работа GC оптимизируется за счет ограничения частоты проверки объектов первого и второго поколений. Объекты нулевого поколения проверяются в каждом цикле работы GC. Примерно в каждом десятом цикле GC проверяются объекты поколения 0 и 1, а в каждом сотом цикле исследуются все объекты. Еще раз задумайтесь о завершении и о том, чего оно стоит: объект, которому необходимо завершение, может оставаться в памяти на девять циклов работы GC дольше, чем в том случае, если ему не нужно завершение. Если же за это время завершение для него все еще не было выполнено, он переходит во второе поколение. Во втором поколении объект может прожить сто дополнительных циклов GC (до следующей сборки мусора для второго поколения).


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