Proceso de Sustitución.Comience con AppDomains


Antecedentes

Tengo un servicio de Windows que utiliza varios archivos DLL de terceros para realizar trabajos en archivos PDF. Estas operaciones pueden utilizar un poco de recursos del sistema, y en ocasiones parecen sufrir fugas de memoria cuando se producen errores. Las DLL son envoltorios administrados alrededor de otras DLL no administradas.

Solución Actual

Ya estoy mitigando este problema en un caso empaquetando una llamada a una de las DLL en una aplicación de consola dedicada y llamándola aplicación a través del Proceso.Empezar(). Si la operación falla y hay fugas de memoria o manejadores de archivos no lanzados, realmente no importa. El proceso terminará y el sistema operativo recuperará los controladores.

Me gustaría aplicar esta misma lógica a los otros lugares de mi aplicación que utilizan estos archivos DLL. Sin embargo, no estoy muy entusiasmado con agregar más proyectos de consola a mi solución, y escribir aún más código de placa de caldera que llame a Proceso.Start() y analiza la salida de la consola aplicaciones.

Nueva Solución

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

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

Curiosamente, agregar un evento DomainUnload vacío al dominio worker hace que la prueba unitaria pase. De todos modos, me preocupa que tal vez la creación de "worker" AppDomains no resolverá 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 ) )
    {
    }
}
Author: MikeWyatt, 2009-10-02

3 answers

Los dominios de aplicación y la interacción entre dominios es un asunto muy delgado, por lo que uno debe asegurarse de que realmente entiende cómo funcionan las cosas antes de hacer nada... Mmm... Digamos, "no estándar": -)

En primer lugar, su método de creación de flujo realmente se ejecuta en su dominio" predeterminado " (surprise-surprise!). ¿Por qué? Simple: el método que pasa a AppDomain.DoCallBack está definido en un objeto AppDomainDelegateWrapper, y ese objeto existe en su dominio predeterminado, por lo que es donde se ejecuta su método. MSDN no dice acerca de esta pequeña "característica", pero es bastante fácil de comprobar: solo establece un punto de interrupción en AppDomainDelegateWrapper.Invoke.

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

Pero, ¿cómo pasa su argumento "func" en el otro dominio para que su método estático pueda recogerlo y ejecutarlo?

La forma más evidente es usar AppDomain.SetData, o puedes rodar la tuya propia, pero independientemente de cómo lo hagas exactamente, hay otra problema: si" func " es un método no estático, entonces el objeto en el que está definido debe pasarse de alguna manera al otro appdomain. Se puede pasar por valor (mientras que se copia, campo por campo) o por referencia (creando una referencia de objeto de dominio cruzado con toda la belleza de la comunicación remota). Para hacer lo anterior, la clase tiene que ser marcada con un atributo [Serializable]. Para hacer esto último, tiene que heredar de MarshalByRefObject. Si la clase no es ninguna de las dos, se lanzará una excepción al intentar pasar el objeto al otro dominio. Sin embargo, tenga en cuenta que pasar por referencia prácticamente elimina toda la idea, porque su método seguirá siendo llamado en el mismo dominio en el que existe el objeto, es decir, el predeterminado.

Concluyendo el párrafo anterior, le quedan dos opciones: pasar un método definido en una clase marcada con un atributo [Serializable] (y tenga en cuenta que el objeto se copiará), o pasar un método estático. Sospecho que, para sus propósitos, necesitará el ex.

Y por si acaso se le ha escapado la atención, me gustaría señalar que su segunda sobrecarga de RunInAppDomain (la que toma Action) pasa un método definido en una clase que no está marcada [Serializable]. ¿No ves ninguna clase allí? No tiene que hacerlo: con delegados anónimos que contienen variables enlazadas, el compilador creará una para usted. Y resulta que el compilador no se molesta en marcar esa clase autogenerada [Serializable]. Desafortunado, pero esto es la vida :-)

Habiendo dicho todo eso (muchas palabras, ¿no? :- ), y asumiendo su voto de no pasar ningún método no estático 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 arreglando ese mecanismo, les voy a decir que no tenía propósito de todos modos.

La cosa es, AppDomains no le ayudará para sus propósitos. Solo se ocupan de los objetos administrados, mientras que el código no administrado puede filtrarse y estrellarse todo lo que quiera. El código no administrado ni siquiera sabe que hay cosas como appdomains. Solo sabe de procesos.

Así que, al final, tu mejor opción sigue siendo tu solución actual: solo genera otro proceso y sé feliz. Y, estaría de acuerdo con las respuestas anteriores, no tiene que escribir otra aplicación de consola para cada caso. Simplemente pase un nombre completo de un método estático y haga que la aplicación de consola cargue su ensamblado, cargue su tipo e invoque el método. En realidad, puede empaquetarlo bastante bien de la misma manera que lo intentó con AppDomains. Puede crear un método llamado algo así como "RunInAnotherProcess", que examinará el argumento, obtendrá el nombre completo del tipo y el nombre del método (mientras se asegura de que el método sea estático) y generará la aplicación de consola, que hará el resto.

 74
Author: Fyodor Soikin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-01 08:47:45

No tiene que crear muchas aplicaciones de consola, puede crear una sola aplicación que recibirá como parámetro el nombre completo del tipo calificado. La aplicación cargará ese tipo y lo ejecutará.
Separar todo en pequeños procesos es el mejor método para disponer realmente de todos los recursos. Un dominio de aplicación no puede disponer de recursos completos, pero un proceso sí.

 7
Author: Shay Erlichmen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-10-19 15:44:41

¿Ha considerado abrir una tubería entre la aplicación principal y las aplicaciones secundarias? De esta manera podría pasar información más estructurada entre las dos aplicaciones sin analizar la salida estándar.

 1
Author: user7116,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-10-02 16:43:55