понедельник, 27 февраля 2012 г.

Не забудьте нажать релиз!

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

Давайте представим, что  мы пишем программку, которая сортирует вордовые документы в папке. В цикле по всем файлам папки открываем документ, читаем первую строку и переносим файл документа в папку с таким именем. Радостно пишем код в стиле NET, обращаемся к объектам и радуемся. Ровно до тех пор, пока не запустим программу. Очень быстро обнаружится, чтоб файлы почему-то не копируются, потому что к ним нет доступа.

Почему же так происходит? Дело в том, что для каждого используемого в управляемом коде COM-объекта (в нашем случае – документа Word), Framework создает для нас управляемую обертку – Runtime Callable Wrapper (RCW). Эта обертка держит ссылку на COM-объект и переадресует ему вызовы. Когда же ссылка освобождается? Правильно, тогда, когда пройдет сборка мусора, в которую попадет эта самая обертка. А когда это произойдет? А когда-нибудь. Если хочется пораньше, то можно вызвать GC.Collect. Только для этого надо быть уверенным, что ссылок на этот объект не осталось. А если осталось – то ничего не получится, объект не удалится. Кстати, даже если ссылок нет, он все равно может еще не удалиться. Нужно не забыть сказать страшное заклинание: GC.WaitForPendingFinalizers(). Это заклинание остановит выполнение потока, пока не вызовутся все финализаторы в других потоках. Т.е. первое решение нашей проблемы заключается в том, чтобы в наш цикл перед переносом файла вставить:

GC.Collect();
GC.WaitForPendingFinalizers();

В принципе, вполне рабочий способ. Только какой-то неизящный. Все-таки, явно обращаться к сборщику мусора не хочется. Тогда остается только одно решение - явно вызывать метод Release у COM объекта. Для этого служит заклинание Marshal.ReleaseComObject. Этот метод принимает в качестве параметра обертку и вызывает Release у соответствующего ей объекта.


До использования .NET я программировал на C++. Я очень активно использовал технологию COM, и, честно говоря, уже забыл когда последний раз явно использовал вызов Release(). Управление ссылками происходило автоматически благодаря умным указателям. Обнаружив такое, не постыжусь этого слова, безобразие в современной среде разработки, был несказанно удивлен.


Для явного управления памятью и ресурсами в C# предназначены интерфейс IDisposable и конструкция using. Они позволяют худо-бедно упростить код, по сути помещая декларацию выделения и освобождение ресурса в одной строчке. Однако RCW объекты не реализуют IDisposable, поэтому все что остается – вручную вызывать Release или сборку мусора. Чтобы хоть как-то упростить себе жизнь при использовании COM объектов из управляемого кода, я сочинил простенький класс ComScope, реализующий IDisposable. Он позволяет использовать все прелести конструкции using для автоматического освобождения ссылок на COM объекты.


Рассмотрим пример с явным вызовом Release:



///обработка контрольных точек из проекта MS Project
static void ProcessMilestones(Project msProject, Action<Task> Processor)
{
    var tasks = msProject.Tasks;
    foreach (var task in tasks.Cast<Task>().Where(t => t != null))
    {
        if (task.IsMilestone())
            Processor(task);
        //освобождаем задачу
        System.Runtime.InteropServices.Marshal.FinalReleaseComObject(task);
    }
    //освобождаем коллекцию задач
    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(tasks);
}

И тот же метод, переписанный с использованием ComScope:



 
///обработка контрольных точек из проекта MS Project
static void ProcessMilestones(Project msProject, Action<Task> Processor)
{
    using (var sc = new ComScope())
    {
        //запоминаем коллекцию задач
        var tasks = sc.Remember(msProject.Tasks);
        foreach (var task in tasks.Cast<Task>().Where(t => t != null))
        {
            //запоминаем задачу
            sc.Remember(task);
            if (task.IsMilestone())
                Processor(task);
        }
    }
}

Что делает этот класс? да почти ничего. Он просто запоминает передаваемые ему указатели на объекты, а в методе Dispose вызывает для них Release в порядке, обратном добавлению. Но он очень сильно упрощает жизнь разработчика, так как позволяет помнить о необходимости освобождения запрошенного COM-объекта только в одном месте – там где он его запросил.



class ComScope : IDisposable
{
    Stack<object> _stack = new Stack<object>();
 
    public T Remember<T>(T obj) where T : class
    {
        if (obj != null)
            _stack.Push(obj);
        return obj;
    }
 
    #region IDisposable Members
 
    public void Dispose()
    {
        while (_stack.Count > 0)
        {
            Marshal.ReleaseComObject(_stack.Pop());
        }
    }
 
    #endregion
}

Комментариев нет:

Отправить комментарий