Pergunta

Fundo

Eu tenho um serviço do Windows que utiliza várias DLLs de terceiros para executar o trabalho em arquivos PDF. Estas operações podem usar um pouco de recursos do sistema, e, ocasionalmente, parece sofrer de vazamentos de memória quando ocorrem erros. As DLLs são geridos wrappers em torno de outras DLLs não gerenciados.

Solução atual

Eu já estou mitigar essa questão em um caso envolvendo uma chamada para uma das DLLs em um aplicativo de console dedicado e chamando esse aplicativo via Process.Start (). Se a operação falhar e há vazamentos de memória ou identificadores de arquivo inéditas, isso realmente não importa. O processo terminará e o sistema operacional irá recuperar as alças.

Eu gostaria de aplicar esta mesma lógica para os outros lugares em meu aplicativo que usam essas DLLs. No entanto, eu não estou muito animado sobre a adição de mais projetos de console para a minha solução, e escrever ainda mais código caldeira-plate que chama Process.Start () e analisa a saída das aplicações de console.

New Solution

Uma alternativa elegante para aplicações de console dedicados e Process.Start () parece ser o uso de AppDomains, como este: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx .

Eu tenho implementado um código semelhante no meu aplicativo, mas os testes de unidade não têm sido promissores. Eu criar um FileStream para um arquivo de teste em um AppDomain separado, mas não eliminá-lo. Eu, então, tentar criar outra FileStream no domínio principal, e ele falhar devido ao bloqueio de arquivo inéditas.

Curiosamente, a adição de um evento DomainUnload vazio para o domínio do trabalhador faz a passagem de teste de unidade. Independentemente disso, eu estou preocupado que talvez criando AppDomains "trabalhador" não vai resolver o meu problema.

Os pensamentos?

O Código

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

O teste de unidade

[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 ) )
    {
    }
}
Foi útil?

Solução

domínios de aplicação e interação entre domínios é uma questão muito fina, por isso deve-se certificar de que ele realmente entende como coisa de trabalho antes de fazer qualquer coisa ... Mmm ... Vamos dizer, "não-padrão": -)

Em primeiro lugar, o seu método de criação de fluxo realmente executa no seu domínio "default" (surpresa-surpresa!). Por quê? Simples: o método que você passa para AppDomain.DoCallBack é definido em um objeto AppDomainDelegateWrapper, e esse objeto existe em seu domínio padrão, de modo que é onde o seu método é executado. MSDN não dizer sobre este pequeno "recurso", mas é fácil o suficiente para verificar:. apenas definir um ponto de interrupção no AppDomainDelegateWrapper.Invoke

Então, basicamente, você tem que se virar sem um objeto "wrapper". Use o método estático para o argumento de DoCallBack.

Mas como você passar o seu argumento "func" para o outro domínio para que o seu método estático pode pegá-lo e executar?

A forma mais evidente é a utilização AppDomain.SetData, ou você pode rolar o seu próprio, mas independentemente de como exatamente você fazê-lo, há um outro problema: se "func" é um método não-estático, em seguida, o objeto que está definido on deve ser de alguma forma passou para o outro appdomain. Ela pode ser passada por valor (considerando que é copiado, campo a campo) ou por referência (criando um objecto de referência de domínio cruzado com toda a beleza de comunicação remota). Para fazer primeiro, a classe tem de ser marcada com um atributo [Serializable]. Para fazer este último, tem que herdar de MarshalByRefObject. Se a classe não é nem, uma exceção será lançada sobre tentativa de passar o objeto para o outro domínio. Tenha em mente, porém, que passar por referência praticamente mata toda a idéia, porque o seu método ainda será chamado no mesmo domínio que o objeto existe no -. Isto é, o padrão

A concluir o parágrafo acima, você é deixado com duas opções: ou passar um método definido em uma classe marcada com um atributo [Serializable] (e ter em mente que o objeto será copiado), ou passar um método estático. Eu suspeito que, para seus propósitos, você vai precisar do primeiro.

E apenas no caso de ter escapado a sua atenção, gostaria de salientar que a sua segunda sobrecarga de RunInAppDomain (aquele que leva Action) passa um método definido em uma classe que não é marcado [Serializable]. Não vejo qualquer classe lá? Você não tem que: com delegados anônimos contendo variáveis ??ligadas, o compilador criará um para você. E isso só acontece que o compilador não se preocupa em marcar esse [Serializable] classe gerado automaticamente. Lamentável, mas esta é a vida: -)

Dito tudo isso (um monte de palavras, não é :-), e assumindo que o seu voto de não passar quaisquer métodos não-estáticos e não-[Serializable], aqui estão seus novos métodos 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 );
    }

Se você ainda está comigo, eu aprecio: -)

Agora, depois de passar tanto tempo na fixação desse mecanismo, eu vou te dizer que é foi sem propósito de qualquer maneira.

A coisa é, AppDomains não irá ajudá-lo para seus propósitos. Eles só cuidar de objetos gerenciados, enquanto o código não gerenciado pode vazar e bater tudo o que quer. código não gerenciado nem sequer sabe que existem coisas como AppDomains. Ele só sabe sobre os processos.

Assim, no final, a sua melhor opção continua a ser a sua solução atual: basta gerar outro processo e ser feliz com isso. E, eu concordo com as respostas anteriores, você não tem que escrever um outro console app para cada caso. Basta passar um nome totalmente qualificado de um método estático, e tem o app carga consola sua montagem, coloque o seu tipo, e invocar o método. Você pode realmente empacotá-lo muito ordenadamente em um muito da mesma forma que você tentou com AppDomains. Você pode criar um método chamado algo como "RunInAnotherProcess", que irá analisar o argumento, obter o nome completo do tipo e nome do método de fora (enquanto certificando-se o método é estático) e desovar o consoleaplicativo, que fará o resto.

Outras dicas

Você não tem que criar muitas aplicações do console, você pode criar um único aplicativo que receberá como parâmetro o nome do tipo totalmente qualificado. O aplicativo irá carregar esse tipo e executá-lo.
Separar tudo em processos minúsculas é o melhor método para realmente eliminar todos os recursos. Um aplicação de domínio não pode fazer recursos completos eliminação, mas um processo pode.

Você considerou abrir uma tubo entre a principal aplicação e as aplicações sub? Desta forma, você poderia passar informações mais estruturado entre as duas aplicações sem analisar a saída padrão.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top