Customizing the Microsoft .NET Framework Common Language Runtime
|
As described, unloading code from a process requires you to unload the application domain in which the code is running. The CLR currently doesn't support unloading individual assemblies or types. There are several scenarios in which an application might want to unload a domain. One of the most common reasons for wanting to unload an application domain is to reduce the working set of a process. This is especially important if your application has high scalability requirements or operates in a constrained memory environment. Another common reason to unload an application domain is to enable code running in your process to be updated on the fly. The ASP.NET host is a great example of using application domains to achieve this effect. As you might know, ASP.NET enables you to update a Web application dynamically by simply copying a new version of one or more of the application's DLLs into the bin directory. When ASP.NET detects a new file, it begins routing requests for that application to a new domain. When it determines that all requests in the old application domain have completed, it unloads the domain. In this way, applications can be updated without restarting the Web server. In many cases, this update is completely transparent to the end user. I show you how to implement this feature in your own applications when I talk about the family of shadow copy properties in Chapter 6. Application domains can be unloaded by calling a static method on System.AppDomain called Unload. AppDomain.Unload takes one parameteran instance of System.AppDomain representing the domain you want to unload. Application domains can also be unloaded from unmanaged code using the CLR hosting interfaces. ICLRRuntimeHost includes a method called UnloadAppDomain that takes the unique numerical identifier of the application domain you want to unload. Calls to AppDomain.Unload or ICLRRuntimeHost::UnloadAppDomain cause the CLR to unload the application domain gracefully. By gracefully, I mean that the CLR unloads the domain in an orderly fashion that lets the application code currently running in the domain reach a natural, predictable endpoint. Specifically, the following sequence of events occurs during a graceful shutdown of an application domain:
These steps are described in the following sections. Step 1: Aborting the Threads Running in the Domain
The CLR begins the process of unloading an application domain by stopping all code that is currently executing in the domain. Threads running in the domain are given the opportunity to complete gracefully rather than being abruptly terminated. This is accomplished by sending a ThreadAbortException to all threads in the domains. Although a ThreadAbortException signals to the application that the thread is going away, it does not provide a mechanism for the application to catch the exception and stop the thread from aborting. A ThreadAbortException is persistent in that although it can be caught, it always gets rethrown by the CLR if an application catches it. Code that is running on a thread destined to be aborted has two opportunities to clean up. First, the CLR runs code in all finally blocks as part of thread termination. Also, the finalizers for all objects are run (see the "Step 3: Running Finalizers" section). Be aware that a ThreadAbortException can be thrown on threads that are currently not even executing in the partially unloaded application domain. If a thread executed in that domain at one point and must return to that domain as the call stack is being unwound, that thread receives a ThreadAbortException. For example, consider the case in which a thread is executing some code in Domain 1. At some later point in time, the thread leaves Domain 1 and begins executing in Domain 2. If AppDomain.Unload is called on Domain 1 at this point, the thread's stack would look something like the stack shown in Figure 5-9. Figure 5-9. A thread's stack on a cross-domain call
Clearly, aborting this thread is necessary because the stack contains addresses of calls that will be invalid after the domain is completely unloaded. Step 2: Raising an Unload Event
After the ThreadAbortExceptions have been thrown and the finally blocks have been run, the CLR raises an event to indicate the domain is unloading. This event can be received in either managed code or unmanaged code. I talk more about how to catch this event later in the chapter. Step 3: Running Finalizers
Earlier in the unloading process, the CLR ran all finally clauses as one way to enable the threads running in the domain to clean up before being aborted. In addition to running finallys, the CLR finalizes all objects that live in the domain. This gives the objects a final opportunity to free all resources allocated while the domain was active. Typically, finalizers are run as a part of a garbage collection. When an object is being collected, all unused objects that reference it are likely being finalized and collected as well. However, the order in which objects are finalized is less well defined when the application domain in which an object lives is being unloaded. As a result, it might be necessary for an object's finalizer to behave differently depending on whether the object is being finalized because the domain is unloaded or as part of a collection. A finalizer can tell the difference between these two cases by calling the IsFinalizingForUnload method on System.AppDomain. As its name implies, IsFinalizingForUnload returns true if the finalizer from which it is called is being run during application domain unload. Clearly, running finalizers and finallys during unload enables code contained in add-ins to be run. By default, the CLR makes no guarantees that this code will actually ever terminate. For example, a finalizer could get stuck in an infinite loop, causing the object to stay alive forever. Without customization, the CLR does not impose any timeouts on thread abort or application domain unload. As a result, an attempt to unload an application domain might never actually finish. Fortunately, the CLR hosting interfaces provide an extensible set of customizations related to the way errors are handled and the way attempts to abort threads and unload domains are handled. For example, a host can use these interfaces to specify timeouts for various actions and to cause the CLR to terminate a thread more forcefully when the timeout expires. A host specifies its policy around error handling and unloading using the ICLRPolicyManager interface. I cover the details of how to specify policy using this interface in Chapter 11. As you've seen, the CLR does its best to enable cleanup code to be run as part of unloading an application domain. Although this helps provide for a clean shutdown, the application's logic is terminated the instant the ThreadAbortException is thrown. As a result, the work the application was doing might be halted prematurely. In the best case, this simply results in a program that safely stops running early. However, it's easy to imagine scenarios in which the premature termination of the application leaves the system in an inconsistent state. As the author of an application that initiates application domain unloads, it's in your best interest to unload a domain only when no active threads are running in the domain. This helps you minimize the times when unloading a domain adversely affects the application. The CLR provides no APIs or other mechanisms to help you determine when a domain is empty. You must build additional logic into your application to determine when requests you've dispatched to different threads have been completed. When they all complete successfully, you know the domain is safe to unload. Step 4: Freeing the Internal CLR Data Structures
After all finalizers have run and no more threads are executing in the domain, the CLR is ready to unload all the in-memory data structures used in the internal implementation. Before this happens, however, the objects that resided in the domain must be collected. After the next garbage collection occurs, the application domain data structures are unloaded from the process address space and the domain is considered unloaded. Exceptions Related to Unloading Application Domains
You should be aware of a few exceptions that might get thrown as a result of unloading an application domain. First, once a domain is unloaded, access to objects that used to live in that domain is illegal. When an attempt to access such an object occurs, the CLR will throw an ApplicationDomainUnloadedException. Second, there are a few cases in which unloading an application domain is invalid. For example, AppDomain.Unload cannot be called on the default domain (remember, it must live as long as the process) or on an application domain that has already been unloaded. In these cases, the CLR throws a CannotUnloadAppDomainException. Receiving Application Domain Unload Events
The CLR raises an event to notify the hosting application that an application domain is being unloaded. This event can be received in either managed or unmanaged code. In managed code, domain unload notifications are raised through the System.AppDomain.DomainUnload event. In unmanaged code, the CLR sends application domain unload notifications to hosts through the IActionOnCLREvent interface. I show you how to use IActionOnCLREvent to catch these notifications in the next section. The AppDomain.DomainUnload event takes the standard event delegate that includes arguments for the object that originated the event (the sender) and any additional data that is specific to that event (the EventArgs). The instance of System.AppDomain representing the application domain that is being unloaded is passed as the sender parameter. The EventArgs are null. The following code snippet shows a simple event handler that prints the name of the application domain that is being unloaded: public static void DomainUnloadHandler(Object sender, EventArgs e) { AppDomain ad = (AppDomain)sender; Console.WriteLine("Domain Unloaded Event fired: " + ad.FriendlyName); }
This event is hooked up to the domain using the standard event syntax: AppDomain ad1 = AppDomain.CreateDomain("Application Domain 1"); ad1.DomainUnload += new EventHandler(DomainUnloadHandler);
Because information about the unloaded domain is passed as a parameter, it's convenient to register the same delegate for all application domains you create. When the event is raised you simply use the sender parameter to tell which domain has been unloaded. Receiving Domain Unload Events Using the IActionOnCLREvent Interface
Listening to domain unload events in managed code is much easier to program and therefore is the approach you're likely to use most. However, you can also receive these events in unmanaged code by providing an object that implements the IActionOnCLREvent interface. IActionOnCLREvent contains one method (OnEvent) that the CLR calls to send an event to a CLR host. Here's the definition of IActionOnCLREvent from mscoree.idl: interface IActionOnCLREvent: IUnknown { HRESULT OnEvent( [in] EClrEvent event, [in] PVOID data ); }
The CLR passes two parameters to OnEvent. The first parameter, event, is a value from the EClrEvent enumeration that identifies the event being fired. The second parameter is the data associated with the event. For application domain unloads, the data parameter is the unique numerical identifier representing the unloaded domain. You register your intent to receive events by passing your implementation of IActionOnCLREvent to the CLR through the ICLROnEventManager interface. As with all CLR-implemented hosting managers, you obtain this interface from ICLRControl as shown in the following code snippet: // Get an ICLRRuntimeHost by calling CorBindToRuntimeEx. ICLRRuntimeHost *pCLR = NULL; hr = CorBindToRuntimeEx(......,(PVOID*) &pCLR); // Get the CLR Control object. ICLRControl *pCLRControl = NULL; pCLR->GetCLRControl(&pCLRControl); // Ask for the Event Manager. ICLROnEventManager *pEventManager = NULL; pCLRControl->GetCLRManager(IID_ICLROnEventManager, (void **)&pEventManager); ICLROnEventManager contains two methods. RegisterActionOnEvent enables you to register your object that implements IActionOnCLREvent with the CLR. When you are no longer interested in receiving events, you can unregister your event handler using UnregisterActionOnEvent. Here's the interface definition for ICLROnEventManager from mscoree.idl: interface ICLROnEventManager: IUnknown { HRESULT RegisterActionOnEvent( [in] EClrEvent event, [in] IActionOnCLREvent *pAction ); HRESULT UnregisterActionOnEvent( [in] EClrEvent event, [in] IActionOnCLREvent *pAction ); }
Notice that both methods take a parameter of type EClrEvent. This enables you to register to receive only those events you are interested in. To demonstrate how to receive application domain events using IActionOnCLREvent, I've modified our boat race host sample to unload an application domain that was created for one of the add-ins. Listing 5-2 shows the updated sample. Listing 5-2. BoatRaceHost.cpp
#include "stdafx.h" #include "CHostControl.h" // This class implements IActionOnCLREvent. An instance of this class // is passed as a "callback" to the CLR's ICLROnEventManager to receive // a notification when an application domain is unloaded. class CActionOnCLREvent : public IActionOnCLREvent { public: // IActionOnCLREvent HRESULT __stdcall OnEvent(EClrEvent event, PVOID data); // IUnknown virtual HRESULT __stdcall QueryInterface(const IID &iid, void **ppv); virtual ULONG __stdcall AddRef(); virtual ULONG __stdcall Release(); // constructor and destructor CActionOnCLREvent() { m_cRef=0; } virtual ~CActionOnCLREvent() { } private: long m_cRef; // member variable for ref counting }; // IActionOnCLREvent methods HRESULT __stdcall CActionOnCLREvent::OnEvent(EClrEvent event, PVOID data) { wprintf(L"AppDomain %d Unloaded\n", (int) data); return S_OK; } // IUnknown methods HRESULT __stdcall CActionOnCLREvent::QueryInterface(const IID &iid,void **ppv) { if (!ppv) return E_POINTER; *ppv=this; AddRef(); return S_OK; } ULONG __stdcall CActionOnCLREvent::AddRef() { return InterlockedIncrement(&m_cRef); } ULONG __stdcall CActionOnCLREvent::Release() { if(InterlockedDecrement(&m_cRef) == 0){ delete this; return 0; } return m_cRef; } int main(int argc, wchar_t* argv[]) { // Start the CLR. Make sure .NET Framework version 2.0 is used. ICLRRuntimeHost *pCLR = NULL; HRESULT hr = CorBindToRuntimeEx( L"v2.0.41013, L"wks", STARTUP_CONCURRENT_GC, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (PVOID*) &pCLR); assert(SUCCEEDED(hr)); // Create an instance of our host control object and register // it with the CLR. CHostControl *pHostControl = new CHostControl(); pCLR->SetHostControl((IHostControl *)pHostControl); // Get the CLRControl object. This object enables us to get the // CLR's OnEventManager interface and hook up an instance of // CActionOnCLREvent. ICLRControl *pCLRControl = NULL; hr = pCLR->GetCLRControl(&pCLRControl); assert(SUCCEEDED(hr)); ICLROnEventManager *pEventManager = NULL; hr = pCLRControl->GetCLRManager(IID_ICLROnEventManager, (void **)&pEventManager); assert(SUCCEEDED(hr)); // Create a new object that implements IActionOnCLREvent and // register it with the CLR. We're only registering to receive // notifications on app domain unload. CActionOnCLREvent *pEventHandler = new CActionOnCLREvent(); hr = pEventManager->RegisterActionOnEvent(Event_DomainUnload, (IActionOnCLREvent *)pEventHandler); assert(SUCCEEDED(hr)); hr = pCLR->Start(); assert(SUCCEEDED(hr)); // Get a pointer to our AppDomainManager running in the default domain. IBoatRaceDomainManager *pDomainManagerForDefaultDomain = pHostControl->GetDomainManagerForDefaultDomain(); assert(pDomainManagerForDefaultDomain); // Enter a new boat in the race. This creates a new application domain // whose id is returned. int domainID = pDomainManagerForDefaultDomain->EnterBoat(L"Boats", L"J29.ParthianShot"); // Unload the domain the boat was just created in. This will // cause the CLR to call the OnEvent method in our implementation of // IActionOnCLREvent. pCLR->UnloadAppDomain(domainID); // Clean up. pDomainManagerForDefaultDomain->Release(); pHostControl->Release(); return 0; }
In this sample, the implementation of IActionOnCLREvent is provided by the CActionOnCLREvent class. Notice the implementation of the OnEvent method is very simpleit just prints out the identifier of the application domain that is being unloaded. In the main program an instance of CActionOnCLREvent is created and registered with the CLR by calling the RegisterActionOnEvent method on the ICLROnEventManager pointer obtained from ICLRControl. To trigger the event handler to be called, an application domain is unloaded using ICLRRuntimeHost::UnloadAppDomain. |
|