Pregunta

Fondo

Tengo un servicio de Windows que utiliza varias DLL de terceros para realizar trabajos en archivos PDF.Estas operaciones pueden consumir bastantes recursos del sistema y, en ocasiones, parecen sufrir pérdidas de memoria cuando se producen errores.Las DLL son envoltorios administrados de otras DLL no administradas.

Solución actual

Ya estoy mitigando este problema en un caso al ajustar una llamada a una de las DLL en una aplicación de consola dedicada y llamar a esa aplicación a través de Process.Start().Si la operación falla y hay pérdidas de memoria o identificadores de archivos no publicados, realmente no importa.El proceso finalizará y el sistema operativo recuperará los identificadores.

Me gustaría aplicar esta misma lógica a otros lugares de mi aplicación que usan estas DLL.Sin embargo, no estoy muy entusiasmado con la idea de agregar más proyectos de consola a mi solución y escribir aún más código repetitivo que llame a Process.Start() y analice la salida de las aplicaciones de la consola.

Nueva solución

Una alternativa elegante a las aplicaciones de consola dedicadas y Process.Start() parece ser el uso de AppDomains, como este: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.

Implementé un código similar en mi aplicación, pero las pruebas unitarias no han sido prometedoras.Creo un FileStream en un archivo de prueba en un AppDomain separado, pero no lo desecho.Luego intento crear otro FileStream en el dominio principal y falla debido al bloqueo del archivo no publicado.

Curiosamente, agregar un evento DomainUnload vacío al dominio de trabajo hace que la prueba unitaria pase.De todos modos, me preocupa que tal vez la creación de AppDomains "trabajadores" no resuelva mi problema.

¿Pensamientos?

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

La prueba unitaria

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

Solución

Los dominios de aplicación y la interacción entre dominios es un asunto muy delgada, por lo que uno debe asegurarse de que realmente entiende lo que el trabajo antes de hacer nada ... mmm ... digamos, "no estándar": -)

En primer lugar, el método de flujo de creación de realidad ejecuta en su dominio "default" (sorpresa, sorpresa!). ¿Por qué? Simple: el método que se pasa en AppDomain.DoCallBack se define en un objeto AppDomainDelegateWrapper, y existe ese objeto en su dominio predeterminado, de modo que es donde su método es ejecutado. MSDN no dice acerca de este pequeño "característica", pero es bastante fácil de comprobar:. Acaba de establecer un punto de interrupción en AppDomainDelegateWrapper.Invoke

Así que, básicamente, tiene que arreglárselas sin un objeto "contenedor". Utilice el método estático para el argumento de DoCallBack.

Pero, ¿cómo se pasa el argumento "Func" en el otro dominio para que su método estático puede recogerlo y ejecutar?

La forma más evidente es el uso de AppDomain.SetData, o se puede liar, pero independientemente de cómo es exactamente lo haces, hay otro problema: si "Func" es un método no estático, entonces el objeto que se define de alguna manera debe ser aprobada en el otro dominio de aplicación. Se puede pasar ya sea por valor (mientras que se copia, campo por campo) o por referencia (la creación de una referencia de objeto entre dominios con toda la belleza de Remoting). Para ello primero, la clase debe estar marcado con un atributo [Serializable]. Para hacer esto último, tiene que heredar de MarshalByRefObject. Si la clase es ni una excepción será lanzada al intento de pasar el objeto a otro dominio. Tenga en cuenta, sin embargo, que el paso por referencia mata a más o menos la idea, porque su método todavía se llamará en el mismo dominio que el objeto existe en -. Es decir, el que viene por defecto

Al concluir el párrafo anterior, que se quedan con dos opciones: o bien pasar a un método definido en una clase marcado con un atributo [Serializable] (y tenga en cuenta que el objeto se copiará), o pasar a un método estático. Sospecho que, para sus fines, tendrá la primera.

Y en caso de que haya escapado a su atención, me gustaría señalar que el segundo sobrecarga de RunInAppDomain (la que lleva Action) pasa a un método definido en una clase que no está marcado [Serializable]. Si no ves la clase hay? Usted no tiene que: con los delegados anónimos que contiene variables ligadas, el compilador creará una para ti. Y da la casualidad de que el compilador no se molesta en señalar que [Serializable] clase autogenerada. Desafortunado, pero esta es la vida: -)

Una vez dicho todo eso (un montón de palabras, ¿no es así :-), y suponiendo que su voto de no pasar ningún métodos no estáticos y no [Serializable], aquí están sus nuevos 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 );
    }

Si todavía estás conmigo, aprecio: -)

Ahora, después de pasar tanto tiempo en la fijación de ese mecanismo, voy a decir que es fue sin propósito de todos modos.

La cosa es, dominios de aplicación no le ayudará para sus propósitos. Que sólo se ocupan de objetos gestionados, mientras que el código no administrado puede tener fugas y chocan todo lo que quiera. código no administrado ni siquiera sabe que hay cosas tales como dominios de aplicación. Sólo se sabe acerca de los procesos.

Así que, al final, su mejor opción sigue siendo la solución actual: acaba de generar otro proceso y ser feliz por eso. Y, estoy de acuerdo con las respuestas anteriores, usted no tiene que escribir una aplicación de consola para cada caso. Sólo tiene que pasar un nombre completo de un método estático, y tiene la aplicación de consola cargar su montaje, cargar su tipo, e invocar el método. En realidad se puede empaquetar bastante cuidadosamente en una gran medida del mismo modo que se trató con dominios de aplicación. Se puede crear un método llamado algo así como "RunInAnotherProcess", que examinará el argumento, obtener el nombre de tipo completo y nombre del método fuera de él (mientras se asegura de que el método es estático) y generar la consolaaplicación, que se encargará del resto.

Otros consejos

Usted no tiene que crear muchas aplicaciones de consola, puede crear una sola aplicación que va a recibir como parámetro el nombre completo de tipo cualificado. La aplicación se carga ese tipo y ejecutarlo.
Separar todo en procesos pequeños es el mejor método para eliminar realmente todos los recursos. Un dominio de aplicación rel="noreferrer"> href="http://en.wikipedia.org/wiki/Application_Domain" no puede hacer disponiendo recursos completos, sino un proceso puede.

Ha considerado la apertura de una tubería entre la aplicación principal y los sub aplicaciones? De esta manera se podría pasar información más estructurada entre las dos aplicaciones sin necesidad de analizar la salida estándar.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top