Pull to refresh

Интересная задачка: повышаем стабильность (robustness) приложений (ч. 2)

Reading time3 min
Views756
Итак, привожу решение проблемы из топика habrahabr.ru/blogs/net/69545 — про гарантированное освобождение неуправляемого ресурса.

Как правильно заметил товарищ adontz, проблема решается при помощи CER.
CER — это технология Constrained Execution Region, при помощи которой можно обезопасить свой код, гарантировав ему то, что на некотором его участке гарантированно _не_ случится асинхронного прерывания типа StackOverflow или ThreadAbort, или «отвалился» jitter — всякое бывает, и не всегда есть возможность оставить внешние ресурсы в консистентном состоянии.

Того!


Другими словами, мы можем «форсировать» рантайм на то, чтобы заблокировать участок код и подготовить все для его выполнения: проверить, что нам хватит памяти на стеке и в оперативке, за-jitt-ить код, и затем — рраз! — выполнить подготовленный код, а потом разобраться, не случилось ли чего.

Чтобы исполнение кода не «отвалилось» в процессе, рантайм накладывает некоторые ограничения на то, чего _нельзя_ делать в регионе CER. И, в общем-то, просто так нельзя делать много чего:
  • исполнять MSIL-oпкод newobj;
  • выполнять боксинг (как мы знаем, он неявно вызывает newobj) (unboxing — можно);
  • вызывать виртуальные методы (если они явно не были подготовлены — как, покажу если надо);
  • вызывать методы через Reflection и обращаться к полям через Transparent Proxies;
  • использовать сериализацию;
  • использовать многомерные массивы(!);
В целом, под CER следует «загонять» маленький критический кусочек кода (как правило, работа с внешними ресурсами), непрерывное исполнение которого должно быть гарантировано. Как раз то, что нам надо.

Итак, вернемся к нашим баранам тяжелым будням, т.е. перейдем к примерам. Чтобы воспользоваться CER на практике, нужно копать в сторону класса-хелпера System.Runtime.CompilerServices.RuntimeHelpers.
Его наиболее важный для нас метод — PrepareConstrainedRegions, и выполняет описанную подготовку — например, проходит по _будущему_ стеку вызовов, выполняя pre-jitt и расставляя stop-the-world отметки (теперь понятно, почему нельзя «в лоб» использовать виртуальные методы и интерфейсы — там динамическая диспетчеризация, и стека вызовов составить невозможно до момента реального исполнения кода). Теперь самое интересное. Использование этого метода должно выглядеть так:
...
System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
try {}
finally {
    // atomic-операции должны идти здесь
}
...
Именно так — пустой блок try, и все непрерываемые операции должны идти в блоке finally!

Для проверки — такой тест:
Thread t = new Thread(new ThreadStart(
        () => {
        RuntimeHelpers.PrepareConstrainedRegions();
        try { }
        finally {					
            while (true) { }
    }}));

    t.Start();
    Thread.Sleep(1000);
    t.Abort();
    t.Join();

Программа работает и не завершается. Если теперь закомментировать try-finally, то программа закончится. Совсем — запрос на ThreadAbortException _не_ будет отложен до конца выхода из блока finally (которого не происходит из-за бесконечного цикла while(true){}), а сработает сразу же (почти сразу же)

Теперь мы знаем, как «починить» наш код из примера с IntPtr:
IntPtr ptr = IntPtr.Zero;
try {
    //...
    System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
    try {	}
    finally {
        ptr = Marshal.AllocHGlobal(500);
    }
     // ..работаем, работаем..
}
finally {
    if( ptr != IntPtr.Zero ) Marshal.FreeHGlobal(ptr);
}
...

На будущее — MSDN советует избегать использования неуправляемых ресурсов. Несколько странный совет в свете того, что одна из целей создания CLR — как раз-таки взаимодействие с унаследованным неуправляемым кодом и ресурсами.

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

Понятно, что я разобрал только то, что помогло мне разобраться с утечкой памяти; но в самом CER я не затронул вот что: контракты на методы, годные для CER, подготовка events для вызова внутри CER, подготовка виртуальных методов, использование Critical Finalizers, FailFast и MemoryBarriers для надежной работы программы и предотвращения утечек неуправляемых ресурсов. Надо ли писать об этом или отослать в MSDN?
Tags:
Hubs:
Total votes 30: ↑24 and ↓6+18
Comments20

Articles