49 votes

Remplacement de Process.Start avec AppDomains

Arrière-plan

J'ai un service Windows qui utilise différentes Dll de tiers pour effectuer des travaux sur les fichiers PDF. Ces opérations peuvent utiliser très peu de ressources système, et parfois semblent souffrir de fuites de mémoire lorsque des erreurs se produisent. Les Dll sont gérés wrappers autour d'autres Dll non gérées.

Solution Actuelle

Je suis déjà atténuer ce problème dans un cas, en l'enveloppant d'un appel à l'une des Dll dans une application console et l'appel que app par le biais de Processus.Start(). Si l'opération échoue et il y a des fuites de mémoire ou inédits de descripteurs de fichiers, il n'a pas vraiment d'importance. Le processus et le système d'exploitation sera de récupérer les poignées.

J'aimerais appliquer cette même logique à d'autres endroits dans mon application qui utilisent ces Dll. Cependant, je ne suis pas terriblement excité à propos de l'ajout de plus de la console de projets de ma solution, et écrit même plus de la chaudière-plaque de code qui appelle le Processus.Start() et analyse de la sortie de la console apps.

Nouvelle Solution

Une alternative élégante à la console dédiée applications et Processus.Start() semble être l'utilisation de domaines d'application, comme ceci: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.

J'ai mis en place un code similaire dans mon application, mais les tests n'ont pas été prometteurs. J'ai créer un FileStream pour un fichier de test dans un autre domaine d'application, mais ne pas la jeter. J'ai ensuite essayer de créer un autre FileStream dans le domaine principal, et il échoue en raison de la inédits de verrouillage de fichier.

Fait intéressant, l'ajout d'un vide DomainUnload événement pour le travailleur de domaine fait l'unité de test. Peu importe, je suis inquiète de ce que peut-être la création d'un "travailleur", de domaines d'application ne suffit pas à résoudre mon problème.

Pensées?

Le Code

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

L'unité de 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 ) )
    {
    }
}

75voto

Fyodor Soikin Points 7907

Les domaines d'Application et les interactions entre les domaines est un très mince affaire, donc on doit s'assurer qu'il comprend vraiment comment les choses fonctionnent avant de faire quoi que ce soit... heu... disons, "non-standard" :-)

Tout d'abord, le flux de création de la méthode exécute sur "par défaut" du domaine (surprise, surprise!). Pourquoi? Simple: la méthode que vous passez dans le domaine d'application.DoCallBack est définie sur une AppDomainDelegateWrapper objet, et cet objet existe sur votre domaine par défaut, c'est donc là que sa méthode est exécutée. MSDN ne pas dire à propos de cette petite "feature", mais il est assez facile à vérifier: il suffit de mettre un point d'arrêt dans AppDomainDelegateWrapper.Invoquer.

Donc, fondamentalement, vous avez à faire sans un "wrapper" de l'objet. Utiliser la méthode statique pour DoCallBack de l'argument.

Mais comment voulez-vous passer vos "func" argument dans l'autre domaine, de sorte que votre méthode statique peut le ramasser et de les exécuter?

La manière la plus évidente consiste à utiliser AppDomain.SetData, ou vous pouvez rouler votre propre, mais quel que soit exactement comment vous le faites, il y a un autre problème: si "func" est un non-statique méthode, l'objet qu'il doit être en quelque sorte passé dans l'autre domaine d'application. Il peut être passé par valeur (alors que c'est copié, champ par champ) ou par référence (la création d'un inter-domaine objet de référence avec toute la beauté de l'accès distant). Pour les anciens, la classe doit être marqué avec un attribut [Serializable]. Faire dernier, il a hériter de MarshalByRefObject. Si la classe n'est ni de, une exception sera levée lors de la tentative de faire passer l'objet à l'autre domaine. Gardez à l'esprit, cependant, que le passage par référence assez bien tue l'idée, parce que votre méthode s'appelle toujours sur le même domaine que l'objet existe - ce qui est, par défaut.

La conclusion du paragraphe ci-dessus, il vous reste deux options: soit de passer à une méthode définie dans une classe marquée avec un attribut [Serializable] (et de garder à l'esprit que l'objet sera copié), ou de passer d'une méthode statique. Je soupçonne que, pour votre application, vous aurez besoin de l'ancien.

Et juste au cas où il a échappé à votre attention, je tiens à souligner que votre deuxième surcharge de RunInAppDomain (celui qui prend des mesures) passe une méthode définie dans une classe qui n'est pas marquée [Serializable]. Ne vois pas la classe? Vous n'avez pas à: avec les délégués anonymes contenant des variables liées, le compilateur va créer un pour vous. Et il se trouve que le compilateur n'a pas pris la peine de marque générée automatiquement classe [Serializable]. Malheureux, mais c'est la vie :-)

Après avoir dit tout cela (beaucoup de mots, n'est-ce pas? :-), et en supposant que votre vœu de ne pas passer n'importe quel non-statique et non[Serializable] les méthodes, voici votre nouveau RunInAppDomain méthodes:

    /// <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 vous êtes toujours avec moi, j'apprécie :-)

Maintenant, après avoir passé beaucoup de temps sur la fixation de ce mécanisme, je vais vous dire que c'est inutile de toute façon.

La chose est, les domaines d'application ne sera pas vous aider pour vos besoins. Ils ne prennent soins d'objets gérés, tandis que le code non managé peut fuir et crash tout ce qu'il veut. Code non managé ne sait même pas il ya des choses telles que les domaines d'application. Il ne sait que sur les processus.

Donc, en fin de compte, votre meilleure option reste de votre solution actuelle: il suffit de générer un autre processus et être heureux à ce sujet. Et, je suis d'accord avec les réponses précédentes, vous n'avez pas à écrire une autre console application pour chaque cas. Il suffit de passer le nom complet d'une méthode statique, et que la console de chargement des applications de votre assemblée, charge de votre type, et d'invoquer la méthode. Vous pouvez réellement paquet assez soigneusement dans un très peu de la même manière que vous avez essayé avec des domaines d'application. Vous pouvez créer une méthode appelé quelque chose comme "RunInAnotherProcess", qui traitera de l'argument le nom complet du type et du nom de la méthode (tout en s'assurant que la méthode est statique) et donnera lieu à l'application de console, qui fera le reste.

Prenez soin et bonne chance.

  • Fyodor

7voto

Shay Erlichmen Points 23645

Il n'est pas nécessaire de créer beaucoup d'applications console, vous pouvez créer une seule application qui recevra en paramètre le nom de type qualifié complet. L'application chargera ce type et l'exécutera.
Séparer le tout en petits processus est la meilleure méthode pour disposer réellement de toutes les ressources. Un domaine d'application ne peut pas éliminer toutes les ressources, mais un processus peut le faire.

2voto

user7116 Points 39829

Avez-vous envisagé d' ouvrir un tuyau entre l'application principale et les sous-applications? De cette manière, vous pouvez transmettre des informations plus structurées entre les deux applications sans analyser la sortie standard.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X