Вопрос

Предыстория

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

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

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

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

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

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

Я реализовал аналогичный код в своем приложении, но модульные тесты не были многообещающими.Я создаю 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 ) )
    {
    }
}
Это было полезно?

Решение

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

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

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

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

Наиболее очевидным способом является использование AppDomain.SetData, или вы можете свернуть свой собственный, но независимо от того, как именно вы это делаете, возникает другая проблема:если "func" является нестатическим методом, то объект, для которого он определен, должен быть каким-то образом передан в другой appdomain.Он может передаваться либо по значению (тогда как он копируется поле за полем), либо по ссылке (создавая междоменную ссылку на объект со всеми прелестями удаленного взаимодействия).Чтобы сделать первое, класс должен быть помечен символом [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 );
    }

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

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

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

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

Другие советы

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top