Wednesday, May 25, 2005 1:59 PM codingsanity

Application Domains & Domain Neutrality

An AppDomain is a unit of code isolation. In Windows 32, the process acts as this unit, where code from one process is stopped by the operating system from accessing code in another process. The only problem is that a process is a very large beast for such a task. Context-switching between processes is expensive and the overhead for having an active process is large.
To circumvent this, the bright sparks at Microsoft have introduced the concept of an Application Domain. Basically this is a lightweight process that exists within an operating system process. Due to the verification that the CLR performs, it can guarantee that code in one AppDomain cannot access code in another directly. As a result we have logical isolation, but not physical.

Now, our happy world is disrupted by a nasty thought. Logical isolation means that each assembly (and it's associated bookkeeping data and static instance data) must be loaded into each AppDomain separately. This makes sense in theory, but can mean a helluva lot of duplication between AppDomains that exist within one process. For this reason domain neutrality was introduced.

What domain neutrality means is that the assemblies which are domain neutral will only be loaded into the process once, and all AppDomains will share a copy. This cuts down unnecessary duplication. However we still get some "duplication" since the data segments are duplicated in order to provide the appearance to our code that the AppDomains have each got their own copy of the assembly. Thus, the static variables of our assembly are not shared across AppDomains, even though the code, JIT images, and lookup tables are.

So what actually happens? Well, when your application starts up, the CLR bootstrapper creates the following:
SystemDomain - responsible for process-wide string interning, creating interface IDs and starting the other AppDomains.
SharedDomain - responsible for managing domain-neutral assemblies. mscorlib is always loaded into this domain.
DefaultDomain - This is where your application plays.

Most applications have only these three AppDomains, but you can create your own at runtime if you wish.

So what is loaded as domain-neutral code? That's a bit trickier. It depends on what option the runtime host used when calling CorBindToRuntimeEx. There's three options here:
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN - Only mscorlib is loaded domain-neutral.
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN - All assemblies will be loaded domain-neutral.
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST - All strong-named assemblies will be loaded domain-neutral.

Interestingly you can actually impact this in your applications execution. When you create an AppDomain, you can specify some options for it's setup, specifically it's LoaderOptimization option. This has four options relating to domain-neutrality: MultiDomain, MultiDomainHost, NotSpecified, SingleDomain.

Surprise, surprise they appear to be the same as the CorBindToRuntimeEx options! Except for that NotSpecified option of course. Well, they are related, the settings you specify for this will override your applications settings for the created AppDomain. The NotSpecified option means that the created AppDomain will use the same setting as the AppDomain that is creating it.

So, what exactly happens if you have different rules between the two AppDomains? How then does the CLR decide whether an assembly is to be loaded domain neutral or not? Each AppDomains settings will apply for that AppDomain. Imagine we have three AppDomains: A, B, and C. A and B will load our assembly domain-neutral, whilst C will load it unshared. This will result in the assembly residing in two places: SharedDomain, and AppDomain C. AppDomain A and B will obviously contain references to the AppDomain in SharedDomain.

Seems simple enough doesn't it? However, there's a hitch. When we load an AppDomain as domain-neutral, all of it's referenced assemblies have to be loaded domain-neutral too. This set of assemblies is referred to as an assemblies binding closure. You might think this would be fine since an assemblies binding closure can't change from AppDomain to AppDomain, right? Wrong. Each AppDomain can have it's own assembly resolution rules.

So where does this leave us? Well, if the binding closures are identical, then all is well and good. However, if the binding closures are different, the CLR will create two copies of our domain-neutral assembly in SharedDomain. Any future AppDomains using this shared assembly will have their binding closures evaluated, and will get a reference to the appropriate copy (or a new copy made).

All this is fun and good, but what impact does it have on you? Well, first off it's a good idea to know what setting your DefaultDomain starts up as. Single Domain is the default .NET setting, whilst ASP.NET uses MultiDomainHost. The next thing to keep in mind is that loading shared assemblies with different binding policies may defeat the purpose of having shared assemblies. The point of shared assemblies is that you're trading a little speed for a smaller memory footprint. If you're throwing away the smaller footprint by having non-shared shared assemblies, then the performance hit isn't going to be worth it.

So how do we work out if an assembly is domain-neutral? Well, in .NET 1.0 and 1.1, you can try and work it out based on knowledge of the assembly coupled with knowing what LoaderOptimization option the AppDomain started with. In .NET 2.0 System.Diagnostics.Loader provides the method AssemblyIsDomainNeutral which will give you this information. In addition .NET 2.0 also provides runtime hosts with much finer grained control than the broad options outlined above. You can implement the GetDomainNeutralAssemblies method of IHostControl to give your host complete control over domain-neutrality.

References:
Junfeng Zhang's brilliant article on domain-neutral assemblies, Chris Brumme's article on AppDomains.

Comments

No Comments