Подтвердить что ты не робот

Замена процесса. Начало с AppDomains

Фон

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

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

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

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

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

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

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

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

Мысли?

Код

/// <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 () );
    }
}

unit test

[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 ) )
    {
    }
}
4b9b3361

Ответ 1

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

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

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

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

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

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

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

Сказав все это (много слов, не так ли?:-), и если ваш обет не пропускать какие-либо нестатические и не-t24 методы, вот ваши новые методы 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 );
    }

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

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

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

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

Ответ 2

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

Ответ 3

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