Saturday, July 05, 2008

In order to solve the problems happen in my application, I decided to dig into the AppDomain. I found some interesting information, so I write them down.

Keys of AppDomain

The CLR scopes all objects, values, and object references to a particular AppDomain. An object resides in exactly one AppDomain, as do values. Moreover, object references must refer to objects in the same AppDomain. Like objects, types reside in exactly one AppDomain. If two AppDomains need to use a type, one must initialize and allocate the type once per AppDomain. Additionally, one must load and initialize the type’s module and assembly once for each AppDomain the type is used in.

Besides, unloading an AppDomain is the only way to unload a module or assembly. Unloading an AppDomain is also the only way to reclaim the memory consumed by a type’s static fields.

I also found some interesting things about the Thread class in .Net framework. System.Threading.Thread represents a schedulable entity in an AppDomain. It is soft thread; it is not recognized by the underlying OS. OS threads are referred to as hard threads. There is no one-to-one relationship between hard threads and CLR soft thread objects. A CLR soft thread object resides in exactly one AppDomain. In the current implementation of CLR, a given hard thread must have at most one soft thread object affiliated with it for a give AppDomain. And it maintains a per-AppDomain thread table to ensure that a given hard thread is affiliated with only one soft thread object per AppDomain.

We can use SetData and GetData to share information between the AppDomains. AppDomain.DoCallBack method allows you to specify a method on a type that will be executed in the foreign domain. This method must comply with the CrossAppDomainDelegate’s signature and must be static.

Table 1 lists several useful AppDomain events.

Table 1 AppDomain Events

Event Name

EventArg Properties

Description

AssemblyLoad

Assembly LoadedAssembly

Assembly has just been successfully loaded

AssemblyResolve

string Name

Assembly reference cannot be resolved

TypeResolve

string Name

Type reference cannot be resolved

ResourceResolve

string Name

Resource reference cannot be resolved

DomainUnload

None

Domain is about to be unloaded

ProcessExit

None

Process is about to shut down

UnhandledException

bool is Terminating, object ExceptionObject

Exception escaped thread-specific handlers

Table 2 lists the properties of an AppDomain that are used by the assembly resolver.

Table 2 AppDomain Environment Properties

AppDomainSetup Property

Get/SetData Name

Description

ApplicationBase

APPBASE

Base directory for probing

ApplicationName

APP_NAME

Symbolic name of application

ConfigurationFile

APP_CONFIG_FILE

Name of.config file

DynamicBase

DYNAMIC_BASE

Root of codegen directory

PrivateBinPath

PRIVATE_BINPATH

Semicolon-delimited list of subdirs

PrivateBinPathProbe

BINPATH_PROBE_ONLY

Suppress probing at APPBASE ("*" or null)

ShadowCopyFiles

FORCE_CACHE_INSTALL

Enable/disable shadow copy (Boolean)

ShadowCopyDirectories

SHADOW_COPY_DIRS

Directories to shadow-copy from

CachePath

CACHE_BASE

Directory the shadow-copy to

LoaderOptimization

LOADER_OPTIMIZATION

JIT-compile per-process or per-domain

DiablePublisherPolicy

DISALLOW_APP

Suppress component-supplied version policy

AppDomain Property

Description

BaseDirectory

Alias to AppDomainSetup.ApplicationBase

RelativeSearchPath

Alias to AppDomainSetup.PrivateBinPath

DynamicDirectory

Directory for dynamic assemblies (/)

FriendlyName

Name of AppDomain used in debugger

AppDomain can also affect the way the JIT compiler works. When the CLR initializes an AppDomain, the CLR accepts a loader optimization flag (System.LoaderOptimization) that controls hwo code is JIT-compiled for modules loaded by that AppDomain. As shown in Table 3, this flag has three possible values.

Table 3 LoaderOptimization Enumeration/Attribute

Value

Expected Domains in Process

Each Domain Expected to Run ...

Code for MSCORLIB

Code for Assemblies in GAC

Code for Assemblies not in GAC

SingleDomain

One

N/A

Per-process

Per-domain

Per-domain

MultiDomain

Many

Same Program

Per-process

Per-process

Per-process

MultiDomainHost

Many

Different Programs

Per-process

Per-process

Per-domain

Marshal and Unmarshal

As I have mentioned before, all objects, values and object references are scoped to a particular AppDomain. When one needs to pass a reference or value to another AppDomain, one must first marshal it. .Net provides RemotingServices.Marshal to do this job. It takes an object reference of type System.Object and returns a serializable System.Runtime.RemotingServices.ObjRef object that can be passed in serialized form. to other AppDomains. Upon receiving a serialized ObjRef, one can obtain a valid object reference using the RemotingServices.Unmarshal method. When calling AppDomain.SetData on a foreign AppDomain, the CLR calls RemotingServices.Marshal. Similarly, calling AppDomain.GetData on a foreign AppDomain returns a marshaled reference, which is converted via RemotingServices.Unmarshal just prior to the method's completion.

If a type derives from System.MarshalByRefObject, then it’s AppDomain-bound, which means the type will marshal by reference and the CLR will give the receiver of the marshaled object (reference) a project that remotes all member access back to the object’s home AppDomain. Note, the proxies never remote static methods. If types don’t derive from MarshalByRefObject but do support object serialization (System.Serializable), they are considered unbound to any AppDomain. They will marshal by value. And the CLR will give the receiver of the marshaled object (reference) a disconnected clone of the original object.

Marshaling typically happens implicitly when a call is made to a cross-AppDomain proxy. The CLR marshals the input parameters to the method call into a serialized request message that the CLR sends to the target AppDomain. When the target AppDomain receives the serialized request, it first deserializes the message and pushes the parameters onto a new stack frame. After the CLR dispatches the method to the target object, the CLR then marshals the output parameters and return value into a serialized response message that the CLR sends back to the caller's AppDomain where the CLR unmarshals them and places them back on the caller's stack.The following diagram show the architecture.

Figure 1. Cross-Domain Method Calls

When one use cross-AppDomain proxies, then both AppDomains must have access to the same assemblies. Moreover, when the two AppDomains reside on different machines, both machines must have access to the shared types’ metadata.

CreateInstance

Now, let’s see what happens when we use AppDomain.CreateInstance method to create remote object in another AppDomain. This method only returns an object handle, which is similar to marshaled object references. And this method returns object handle rather than a real object reference, this can avoid requiring metadata in the caller’s AppDomain, in other words, in the caller’s AppDomain, it does need to load the assembly. An attempt to call CreateInstance on a target application domain that is not the current application domain will result in a successful load of the assembly in the target application domain. Since an Assembly is not MarshalByRefObject, when this method attempts to return the Assembly for the loaded assembly to the current application domain, the common language runtime will try to load the assembly into the current application domain and the load might fail. Actually, AppDomain.CreateInstance method delegates the work to Activator.CreateInstance; Internally, Activator loads the assembly first, then it tries find the class’s public constructors; after that, it checks whether the type can be marshaled. Then, it continues to use RuntimeType (internal class) to create the object. Finally, it wraps this object in an ObjectHandle object, and returns this ObjectHandle object back to the parent AppDomain.

Unload

Another thing I am concerned is how to unload an AppDomain. AppDomain.Unload method is used to unload the whole AppDomain. In the .NET Framework version 2.0 there is a thread dedicated to unloading application domains. This improves reliability, especially when the .NET Framework is hosted. When a thread calls Unload, the target domain is marked for unloading. The dedicated thread attempts to unload the domain, and all threads in the domain are aborted. If a thread does not abort, for example because it is executing unmanaged code, or because it is executing a finally block, then after a period of time a CannotUnloadAppDomainException is thrown in the thread that originally called Unload. If the thread that could not be aborted eventually ends, the target domain is not unloaded. Thus, in the .NET Framework version 2.0 domain is not guaranteed to unload, because it might not be possible to terminate executing threads. I try to use Reflector to dig into the .net code. I find that it needs to get the AppDomain ID first. In this method, it uses RemotingServices.IsTransparentProxy to see whether the AppDomain is a transparent proxy or a real object. If it is, it gets the AppDomain ID from the proxy by invoking RemotingServices.GetServiceDomainIdForProxy method; else, it gets the ID directly from the AppDomain.Id. After that, it can use this ID to unload the AppDomain. But, I can’t find out what really happens internally.


1 comment:

Anonymous said...

I truly appreciate it.