Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Увлекательные моменты в C# ч.2

Anna | 17.06.2014 | нет комментариев
В данной статье я хотел бы рассказать о некоторых особенностях представления объектов в памяти в .Net, оптимизациях, проводимых компиляторах, и продолжить традицию товарища mynameco, написавшего эту статью

Данный пост не ориентирован на кулхацкеров, следственно если вы знаете, что using компилируется с конструкцией вызова Dispose для энумератора, что для работы оператора foreach не непременно применять интерфейсы, а довольно иметь способ GetEnumerator, возвращающий энумератор правильной сигнатуры, что сам Enumerator — изменяемая (мутабельная) конструкция, что может стать поводом непредвиденного бага… То просьба не заходить и не читать, сэкономьте свое время, не нужно оставлять посты как бы «КГ\АМ», «боян», и «Капитаны отаке». Остальных умоляю под кат.

Управление памятью в .Net

В целом, по этой теме написано много, следственно остановимся на этом лаконично:

когда приложение только стартует, в нем создаются так называемые корни GC. Корни необходимы для построения графа объектов, тот, что выглядит приблизительно так:

image

Как правило, он строится в отдельном потоке, параллельном потоку выполнения. Помимо того, данный поток маркирует объекты, которые недоступны из корней GC (если на объект нет ссылок из корней в данный момент, то и в грядущем на него никто не сумеет сослаться) как «подлежащие удалению» (на картинке узлы 9 и 10). Когда же память, занимаемая программой, превышает некоторый предел либо вызывается способ GC.Collectначинается собственно сборка мусора.

подробнее дозволено ознакомиться в этой статье.

К чему все это?

А вот к чему. Дело в том, что

Объект может быть удален до того, как отработает способ, тот, что данный объект вызывает.

Представим, что у нас есть такой вот простенький класс:

public class SomeClass
    {
        public int I;

        public SomeClass(int input)
        {
            I = input;
            Console.WriteLine("I = {0}", I);
        }

        ~SomeClass()
        {
            Console.WriteLine("deleted");
        }

        public void Foo()
        {
            Thread.Sleep(1000);
            Console.WriteLine("Foo");
        }
    }

а вызывать его мы будем таким образом:

public class Program
    {
        private static void Main()
        {
            new Thread(() =>
                       {
                           Thread.Sleep(100);
                           GC.Collect();
                       }) {IsBackground = true}.Start();
            new SomeClass(10).Foo();
        }
    }}

поток необходим для того, Дабы сборщик удалил объект сразу позже того, как это будет допустимо. В действительности GC, безусловно же, отменнее не трогать, эвристики, на которых он работает, разрабатывали крайне неглупые люди, и ручной вызов способа GC.Collect только вредит и показывает на задачи архитектуры.

Запустим эту программу и получим такой вот итог (компилировать необходимо в режиме release, Дабы дозволить компилятору провести оптимизацию):
image
в действительности, безусловно, такое получить трудно, CLR (ну либо DLR) должна решить провести сборку именно в момент выполнения этого способа, но согласно спецификации это абсолютно допустимо!

Это работает потому, что способы экземпляров ничем не отличаются от статических способов, помимо того, что в нем передается спрятанный параметр — ссылка this, которая ничем не отменнее остальных параметров способа. Ну и еще небольшие различия есть с точки зрения CIL (способы экземпляров неизменно вызываются с поддержкой callvirt, даже те, которые не помечены как виртуальные, когда как для статических способов применяется примитивный call),

Особенности деструкторов
Деструкторы в C#: правда либо миф?

И да, и нет. С одной стороны, онидюже схожи наружно на деструкторы С (и даже пишутся с тем же значком — тильдой), но на самом деле мы переопределяем способ Finalize, унаследованный от класса Object (кстати, обратите внимания, что у деструктора нет модификаторы publicprivate либо какого-либо еще). Следственно деструкторы в C# Почаще называют финализаторами.

Вопрос из зрительского зала: а для чего тогда заморачиваться с Dispose, если в C# есть такие красивые средства, ничем не уступающие деструкторам C ?

Армянское радио отвечает: задача в сборщике. Дело в том, что из-за наличия финализатора, объект должен удаляться в два прохода: вначале отрабатывает финализатор (в отдельном потоке), и только при дальнейшей сборке мусора дозволено будет освободить память, занимаемой объектом. Из-за этого объектам свойственен так называемый «коллапс среднего возраста» — когда дюже много объектов попадает во второе (последнее) поколение, в итоге сборщик начинает дюже крепко тормозить, т.к. взамен стандартной чистки только нулевого поколения он вынужден просматривать всю память. Трактование не претендует на полноту, но по ссылке выше эти вопросы достаточно недурно разобраны, а длинные простыни с капитанскими откровениями редко кому нравится читать.

Следственно отличной практикой считается вызывать способ Dispose у всех объектов, реализующих интерфейс IDisposable. Правда, множество классов стандартной библиотеки на каждый случай имеют финализатор (охрана «от дурака») тот, что имеет такой вид:

    public virtual void Close() // На примере класса Stream
    {
      this.Dispose(true);
      GC.SuppressFinalize((object) this);
    }

    public void Dispose()
    {
      this.Close();
    }
И к чему все это?

А сейчас вооружившись умениями об устройстве финализаторов, мы можем полновесно «воскрешать» объекты!

Перепишем финализатор таким образом:

        ~SomeClass()
        {
            Console.WriteLine("deleted");
            Program.SomeClassInstance = this;
            GC.ReRegisterForFinalize(this);
        }

и немножко изменим Program

    public class Program
    {
        public static SomeClass SomeClassInstance;
        private static void Main()
        {
            new Thread(() =>
                       {
                           Thread.Sleep(100);
                           GC.Collect();
                       }) {IsBackground = true}.Start();
            var wr = new WeakReference(new SomeClass(10));
            Console.WriteLine("IsAlive = {0}", wr.IsAlive);
            ((SomeClass)wr.Target).Foo();
            Console.WriteLine("IsAlive = {0}", wr.IsAlive);
        }
    }

Сейчас позже запуска мы получим беспредельно печатающуюся строчку deleted, потому что объект будет непрерывно удаляться, воскрешаться, вновь удаляться, и так до бесконечности… (Немножко схоже на Skeleton King’a с аегисом на 3 заряда :) )

image

Финализатор может быть не вызван никогда

Ну самое примитивное, это если программа закрывается принудительно через диспетчер задач либо иным некошерным образом (еще один плюс в копилку IDisposable). А вот в каких еще случаях он не будет вызыван?

Результат

Скажем, если программа запускается 1-й раз и объекты не удалялись, то взамен способа финализатора будет способ-заглушка (дерзкий компиляцию основного тела способа). Тогда, если если в один очаровательный момент у системы не будет хватать памяти и она с крахом упадет, то в различие от других сходственных случаев, финализатор не вызовется легко потому, что у JIT’а не будет памяти, Дабы разместить взамен заглушки код этого самого финализатора. Помимо того, финализатор всякого объекта может выполнятся не больше 2-х секунд, а каждой очереди финализации дается не больше 40 секунд.

Взамен завершения

С одной стороны, подытожить необходимо все это, с иной — как бы все теснее рассказано. Пытался с одной стороны как-то написать про увлекательные вероятности, с иной — не крепко капитанить на тему того, что теснее дюже недурно расписано в других статьях. Что получилось, решать вам.

P.S. Принимаются всякие примечания по смысловым/орфографическим/пунктуационным/синтаксическим/семантическим/морфологическим и прочим ошибкам.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB