Tuesday, February 09, 2010

Running code in a separate AppDomain

Suppose you’ve got a chunk of code that you need to run as part of your application but you’re concerned that it might bring down your app or introduce a memory leak.  Fortunately, the .NET runtime provides an easy mechanism to run arbitrary code in a separate AppDomain.  Not only can you isolate all exceptions to that AppDomain, but when the AppDomain unloads you can reclaim all the memory that was consumed.

Here’s a quick walkthrough that demonstrates creating an AppDomain and running some isolated code.

Create a new AppDomain

First we’ll create a new AppDomain based off the information of the currently running AppDomain.

AppDomainSetup currentSetup = AppDomain.CurrentDomain.SetupInformation;

var info = new AppDomainSetup()
              {
                  ApplicationBase = currentSetup.ApplicationBase,
                  LoaderOptimization = currentSetup.LoaderOptimization
              };

var domain = AppDomain.CreateDomain("Widget Domain", null, info);

Unwrap your MarshalByRefObject

Next we’ll create an object in that AppDomain and serialize a handle to it so that we can control the code in the remote AppDomain.  It’s important to make sure the object you’re creating inherits from MarshalByRefObject and is marked as serializable.  If you forget this step, the entire object will serialize over to the original AppDomain and you lose all isolation.

string assemblyName = "AppDomainExperiment";
string typeName = "AppDomainExperiment.MemoryEatingWidget";

IWidget widget = (IWidget)domain.CreateInstanceAndUnwrap(assemblyName, typeName);

Unload the domain

Once we’ve finished with the object, we can broom the entire AppDomain which frees up all resources attached to it.  In the example below, I’ve deliberately created a static reference to an object to prevent it from going out of scope.

AppDomain.Unload(domain);

Putting it all together

Here’s a sample that shows all the moving parts.

namespace AppDomainExperiment
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [Test]
    public class AppDomainLoadTests
    {
        [TestMethod]
        public void RunMarshalByRefObjectInSeparateAppDomain()
        {
            Console.WriteLine("Executing in AppDomain: {0}", AppDomain.CurrentDomain.Id);
            WriteMemory("Before creating the runner");

            using(var runner = new WidgetRunner("AppDomainExperiment",
                                                "AppDomainExperiment.MemoryEatingWidget"))
            {

                WriteMemory("After creating the runner");

                runner.Run(Console.Out);

                WriteMemory("After executing the runner");
            }

            WriteMemory("After disposing the runner");
        }

        private static void WriteMemory(string where)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            long memory = GC.GetTotalMemory(false);

            Console.WriteLine("Memory used '{0}': {1}", where, memory.ToString());
        }
    }

    public interface IWidget
    {
        void Run(TextWriter writer);
    }

    public class WidgetRunner
    {
        private readonly string _assemblyName;
        private readonly string _typeName;
        private AppDomain _domain;

        public WidgetRunner(string assemblyName, string typeName)
        {
            _assemblyName = assemblyName;
            _typeName = typeName;
        }

        #region IWidget Members

        public void Run(TextWriter writer)
        {
            AppDomainSetup currentSetup = AppDomain.CurrentDomain.SetupInformation;

            var info = new AppDomainSetup()
                          {
                              ApplicationBase = currentSetup.ApplicationBase,
                              LoaderOptimization = currentSetup.LoaderOptimization
                          };

            _domain = AppDomain.CreateDomain("Widget Domain", null, info);

            var widget = (IWidget)_domain.CreateInstanceAndUnwrap(_assemblyName, _typeName);

            if (!(widget is MarshalByRefObject))
            {
                throw new NotSupportedException("Widget must be MarshalBeRefObject");
            }
            widget.Run(writer);
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            AppDomain.Unload(_domain);
        }

        #endregion
    }

    [Serializable]
    public class MemoryEatingWidget : MarshalByRefObject, IWidgetModule
    {
        private IList<string> _memoryEater;

        private static IWidgetModule Instance;

        #region IAppLauncher Members

        public void Run(TextWriter writer)
        {
            writer.WriteLine("Executing in AppDomain: {0}", AppDomain.CurrentDomain.Id);

            _memoryEater = new List<string>();

            // create some really big strings
            for(int i = 0; i < 100; i++)
            {
                var s = new String('c', i*100000);
                _memoryEater.Add(s);
            }

            // THIS SHOULD PREVENT THE MEMORY FROM BEING GC'd
            Instance = this;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            
        }

        #endregion
    }
}

Running the test shows the following output:

Executing in AppDomain: 2
Memory used 'Before creating the runner': 569060
Memory used 'After creating the runner': 487508
Executing in AppDomain: 3
Memory used 'After executing the runner': 990525340
Memory used 'After disposing the runner': 500340

Based on this output, the main take away is that the memory is reclaimed when the AppDomain is unloaded.  Why do the numbers not match up in the beginning and end?  It’s one of those mysteries of the managed garbage collector, it reminds me of my favorite Norm McDonald joke from SNL:

“Who are safer drivers? Men, or women?? Well, according to a new survey, 55% of adults feel that women are most responsible for minor fender-benders, while 78% blame men for most fatal crashes. Please note that the percentages in these pie graphs do not add up to 100% because the math was done by a woman. [Crowd groans.] For those of you hissing at that joke, it should be noted that that joke was written by a woman. So, now you don't know what the hell to do, do you? [Laughter] Nah, I'm just kidding, we don't hire women”

Happy Coding.

submit to reddit

2 comments:

Yort said...

Unhappily, it appears that from .Net 2.0 on app domains DO NOT isolate unhandled exceptions (do a Google search for the keywords "isolating exceptions in appdomains" for more info.

An *unhandled* exception in a secondary app domain will still cause the parent domain to unload and the app to shutdown, unless you enable legacy exception handling in the config file (which is generally not recommended). It also seems there is no way to prevent that behaviour.

If you really want to provide that level of isolation you need to host the clr yourself so you can change the way unhandled exceptions are processed. This seems like overkill for a lot of situtations.

You can reclaim memory (and unload assemblies) using appdomains like your post suggests though. So other than the suggestion that apddomains isolate exceptions, good article.

bryan said...

Good point, Yort. I hadn't tried that out in this post, though it is possible to set up an exception handler for each app domain. I wonder if adding an anonymous delegate to swallow and mark the exception as handled would prevent it from bubbling up to the parent app domain.