All .NET components and applications require a
managed environment to run in. However, the underlying operating system
knows nothing about managed code; it provides processes
only. Processes are also unaware that .NET exists; they provide raw
elements such as memory, handle tables, and so on. Managed code
therefore can't execute directly in the native operating system
process—there is a need for a bridge between managed code and unmanaged
code. The bridging link is a concept called an application domain, or app domain.
You can think of the app domain as the .NET equivalent of a process,
with one important difference: an app domain is built on top of the
unmanaged process, and there is no requirement for one-to-one mapping
between app domains and operating system processes. As a result, a single physical process can actually host multiple app domains (see Figure 1).
1. App Domains Versus Physical Processes
App domains are better perceived as logical
processes, instead of real processes. The fact that a single physical
process can host multiple app domains yields important benefits. The
main reason why developers resorted to multiple processes in the past
was to provide fault isolation. If all the components of an application
and their clients are in the same process and a component has a fatal
error that crashes the process, it brings down the entire application,
including the clients. Similarly, if the client has a fatal error, the
components go down with it. By distributing the clients and servers of
an application to separate processes, an application achieves fault
isolation—in the event of a fault only the culprit process goes down,
allowing you to handle the error or perform a graceful exit.
Another reason for
distributing the components of an application across processes is
security. Server objects are often called on to authenticate incoming
client calls or to perform access control and authorization before
allowing a given call to access a component. Having separate processes
allows for separate security identifiers for each process and for the
enforcement of authentication on cross-process calls. Unfortunately,
however, there are significant penalties to using multiple processes:
Creating a new process is time-consuming, as is the disposal of an existing process.
Keeping
a process running is expensive, both in terms of memory and of the
resources the operating system allocates to each process. Having too
many processes running can significantly degrade system performance.
Making
a cross-process call involves a call penalty, because crossing a
process boundary is very expensive compared to making a direct call.
Cross-process calls rely on special mechanisms such as named pipes,
sockets, and LPC/RPC.
Coding
is more complex—the client's code for making a direct local call on an
object is very different from that of making the same call on the object
in a different process.
Compared with
traditional unmanaged processes, .NET app domains can provide
single-process performance with lower overhead. They can also provide
the isolation and other benefits of multiple processes, even if they
share the same physical process. You can start and shut down app domains
independently of their hosting processes, and you can even debug them
separately. For example, all ASP.NET web applications share the same
physical worker process by default, but each web application is put in
its own dedicated app domain. The time it takes to create or destroy an
app domain is a fraction of that required for a physical process, and
keeping an app domain alive is considerably cheaper. Furthermore,
cross-app domain calls in the same process are faster than cross-process
calls. .NET also maintains a strict security boundary between app
domains, so objects in one app domain can't interfere with the objects
(or data) in another, unless the objects agree to cooperate using .NET
remoting.
In unmanaged C++,
static variables are visible to all clients in the same process. In C#,
each app domain gets its own separate set of static variables. |
|
In the interest of
fault isolation and security, each app domain loads and maintains its
own set of assemblies. Consider, for example, the app domains in Figure 10-1. Because App Domain B and App Domain C require the class library Assembly 1, on Windows .NET loads Assembly 1 twice and gives each app domain its own copy. This allows clients in each app domain to interact with Assembly 1 independently of other clients in other app domains.
2. App Domains and the .NET Platform
The .NET runtime itself is a
set of Windows DLLs, implemented in unmanaged C++. These DLLs provide
the managed heap, garbage collector, JIT compiler, assembly resolver and
loader, and all the other elements that make managed code possible. The
app domain merely enables the assemblies it loads to access these
services (see Figure 2)—in
effect, this is how the app domain bridges the unmanaged world and the
managed world. However, it's important to note that all app domains in
the same process share the same managed heap.
2.1. App domains and threads
.NET managed threads have
no app domain affinity, meaning that a thread can enter and exit any app
domain that runs in the same underlying process. Typically, when you
create a thread in your app domain, that thread executes a thread method
and accesses only local objects. However, nothing prevents you from
having threads created in one app domain access objects in another app
domain in the same process. There is one detail you need to be aware of,
though: when an app domain shuts down (i.e., when AppDomain.Unload( ) is called), it terminates all the threads that happen to be calling objects in it by calling Thread.Abort( ) on each of them.
3. App Domains and Remoting
Like traditional cross-process calls, you make cross-app domain calls using remoting,
a programmatic act that accesses an object outside its hosting app
domain. .NET uses exactly the same remote-call architecture for all
cases, whether the cross-app domain call is between two app domains in
the same process, between app domains in two different processes on the
same machine, or between app domains on two separate machines (see Figure 3).
Clients in the same app domain as the called object can each have a direct reference to the object (see Figure 3). Clients in a different app domain use a proxy to connect to the object. A proxy
is an object that provides exactly the same interfaces, public methods,
properties, and members as the real object. .NET generates the proxy on
the fly, based on the object's metadata. Even though the proxy has the
same public entry points as the object, it can't serve the clients
because the object's actual code and state reside only where the object
is. All the proxy knows is how to bind to the object and forward the
calls made on the proxy to the object. Forwarding a call to an object is
called marshaling.
Marshaling is a nontrivial feat: its end goal is to provide the client
with the illusion that it's calling a local object and to provide the
server with the illusion that it's servicing a local client. Neither the
server nor the client explicitly uses remote mechanisms such as pipes,
RPC, or sockets, because these details are encapsulated in the proxy.
.NET does require, however, that if an object is accessed by proxy, the
object's class must derive directly or indirectly from the abstract
class MarshalByRefObject.