Замена Process.Start с доменами приложений - PullRequest
49 голосов
/ 02 октября 2009

Фон

У меня есть служба Windows, которая использует различные сторонние библиотеки DLL для выполнения работы с файлами PDF. Эти операции могут использовать довольно много системных ресурсов, и иногда, кажется, страдают от утечек памяти при возникновении ошибок. Библиотеки DLL являются управляемыми оболочками вокруг других неуправляемых библиотек DLL.

Текущее решение

Я уже смягчил эту проблему в одном случае, заключив вызов одной из библиотек DLL в специальном консольном приложении и вызвав это приложение через Process.Start (). Если операция завершается неудачно и возникают утечки памяти или невыпущенные дескрипторы файлов, это не имеет значения. Процесс завершится, и ОС восстановит дескрипторы.

Я хотел бы применить ту же логику к другим местам в моем приложении, которые используют эти библиотеки DLL. Тем не менее, я не сильно рад добавлению в консоль новых консольных проектов и написанию еще большего кода, который вызывает Process.Start () и анализирует выходные данные консольных приложений.

Новое решение

Элегантной альтернативой выделенным консольным приложениям и Process.Start (), по-видимому, является использование доменов приложений, например: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.

Я реализовал подобный код в своем приложении, но модульные тесты не были многообещающими. Я создаю FileStream для тестового файла в отдельном домене приложений, но не располагаю им. Затем я пытаюсь создать еще один FileStream в главном домене, и он завершается неудачно из-за невыпущенной блокировки файла.

Интересно, что добавление пустого события DomainUnload в рабочий домен делает прохождение модульного теста. Несмотря на это, я обеспокоен тем, что, возможно, создание «рабочих» доменов приложений не решит мою проблему.

Мысли

Код

/// <summary>
/// Executes a method in a separate AppDomain.  This should serve as a simple replacement
/// of running code in a separate process via a console app.
/// </summary>
public T RunInAppDomain<T>( Func<T> func )
{
    AppDomain domain = AppDomain.CreateDomain ( "Delegate Executor " + func.GetHashCode (), null,
        new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory } );

    domain.DomainUnload += ( sender, e ) =>
    {
        // this empty event handler fixes the unit test, but I don't know why
    };

    try
    {
        domain.DoCallBack ( new AppDomainDelegateWrapper ( domain, func ).Invoke );

        return (T)domain.GetData ( "result" );
    }
    finally
    {
        AppDomain.Unload ( domain );
    }
}

public void RunInAppDomain( Action func )
{
    RunInAppDomain ( () => { func (); return 0; } );
}

/// <summary>
/// Provides a serializable wrapper around a delegate.
/// </summary>
[Serializable]
private class AppDomainDelegateWrapper : MarshalByRefObject
{
    private readonly AppDomain _domain;
    private readonly Delegate _delegate;

    public AppDomainDelegateWrapper( AppDomain domain, Delegate func )
    {
        _domain = domain;
        _delegate = func;
    }

    public void Invoke()
    {
        _domain.SetData ( "result", _delegate.DynamicInvoke () );
    }
}

Юнит тест

[Test]
public void RunInAppDomainCleanupCheck()
{
    const string path = @"../../Output/appdomain-hanging-file.txt";

    using( var file = File.CreateText ( path ) )
    {
        file.WriteLine( "test" );
    }

    // verify that file handles that aren't closed in an AppDomain-wrapped call are cleaned up after the call returns
    Portal.ProcessService.RunInAppDomain ( () =>
    {
        // open a test file, but don't release it.  The handle should be released when the AppDomain is unloaded
        new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None );
    } );

    // sleeping for a while doesn't make a difference
    //Thread.Sleep ( 10000 );

    // creating a new FileStream will fail if the DomainUnload event is not bound
    using( var file = new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ) )
    {
    }
}

Ответы [ 3 ]

75 голосов
/ 02 октября 2009

Домены приложений и междоменное взаимодействие - очень тонкий вопрос, поэтому нужно убедиться, что он действительно понимает, как все работает, прежде чем что-то делать ... Ммм ... Скажем, "нестандартный": -)

Прежде всего, ваш метод создания потока фактически выполняется в вашем домене по умолчанию (сюрприз-сюрприз!). Зачем? Все просто: метод, который вы передаете в AppDomain.DoCallBack, определен для объекта AppDomainDelegateWrapper, и этот объект существует в вашем домене по умолчанию, поэтому его метод выполняется. MSDN не говорит об этой маленькой «функции», но ее достаточно легко проверить: просто установите точку останова в AppDomainDelegateWrapper.Invoke.

Так что, по сути, вы должны обходиться без объекта-обертки. Используйте статический метод для аргумента DoCallBack.

Но как передать свой аргумент "func" в другой домен, чтобы ваш статический метод мог его взять и выполнить?

Наиболее очевидный способ - использовать AppDomain.SetData, или вы можете бросить свой собственный, но независимо от того, как именно вы это делаете, есть другая проблема: если «func» - это нестатический метод, тогда объект, который он должен быть каким-то образом передан в другой домен приложения. Он может быть передан либо по значению (тогда как оно копируется, поле за полем), либо по ссылке (создавая междоменную ссылку на объект со всей красотой Remoting). Прежде всего, класс должен быть помечен атрибутом [Serializable]. Для этого он должен наследовать от MarshalByRefObject. Если класс не равен ни одному, исключение будет выдано при попытке передать объект в другой домен. Имейте в виду, однако, что передача по ссылке в значительной степени убивает саму идею, потому что ваш метод будет по-прежнему вызываться в том же домене, в котором находится объект, то есть по умолчанию.

Завершая вышеприведенный абзац, у вас остается две опции: либо передать метод, определенный для класса, помеченного атрибутом [Serializable] (и помните, что объект будет скопирован), либо передать статический метод. Я подозреваю, что для ваших целей вам понадобится первое.

И на всякий случай, если это ускользнуло от вашего внимания, я хотел бы отметить, что ваша вторая перегрузка RunInAppDomain (та, которая занимает Action) передает метод, определенный в классе, который не помечен [Serializable]. Не видите там ни одного класса? Вам не нужно: с анонимными делегатами, содержащими связанные переменные, компилятор создаст их для вас. И так уж получилось, что компилятор не потрудился пометить этот автоматически сгенерированный класс [Serializable]. Жаль, но это жизнь: -)

Сказав все это (много слов, не так ли? :-), и принимая ваше обещание не передавать какие-либо нестатические и не [Serializable] методы, вот ваши новые RunInAppDomain методы:

    /// <summary>
    /// Executes a method in a separate AppDomain.  This should serve as a simple replacement
    /// of running code in a separate process via a console app.
    /// </summary>
    public static T RunInAppDomain<T>(Func<T> func)
    {
        AppDomain domain = AppDomain.CreateDomain("Delegate Executor " + func.GetHashCode(), null,
            new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory });

        try
        {
            domain.SetData("toInvoke", func);
            domain.DoCallBack(() => 
            { 
                var f = AppDomain.CurrentDomain.GetData("toInvoke") as Func<T>;
                AppDomain.CurrentDomain.SetData("result", f());
            });

            return (T)domain.GetData("result");
        }
        finally
        {
            AppDomain.Unload(domain);
        }
    }

    [Serializable]
    private class ActionDelegateWrapper
    {
        public Action Func;
        public int Invoke()
        {
            Func();
            return 0;
        }
    }

    public static void RunInAppDomain(Action func)
    {
        RunInAppDomain<int>( new ActionDelegateWrapper { Func = func }.Invoke );
    }

Если ты все еще со мной, я ценю: -)

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

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

Итак, в конце концов, вашим лучшим вариантом остается текущее решение: просто создайте другой процесс и будьте довольны этим. И, я бы согласился с предыдущими ответами, вам не нужно писать другое консольное приложение для каждого случая. Просто передайте полное имя статического метода, и консольное приложение загрузит вашу сборку, загрузит ваш тип и вызовет метод. На самом деле вы можете упаковать его довольно аккуратно, почти так же, как вы пытались использовать с доменами приложений. Вы можете создать метод с именем что-то вроде «RunInAnotherProcess», который будет проверять аргумент, извлекать из него полное имя типа и метода (при этом удостоверившись, что метод статический) и порождая консольное приложение, которое сделает все остальное.

7 голосов
/ 02 октября 2009

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

2 голосов
/ 02 октября 2009

Рассматривали ли вы открытие канала между основным приложением и вспомогательными приложениями? Таким образом, вы можете передавать более структурированную информацию между двумя приложениями, не анализируя стандартный вывод.

...