Section 8.5. Automatic Synchronization


8.5. Automatic Synchronization

To understand the ways in which .NET can automatically synchronize access to components, you must first understand the relationship that .NET maintains between components, processes, application domains, and contexts. When a .NET application starts, the operating system launches an unmanaged process, which loads the .NET runtime. However, a .NET application can't execute directly in the unmanaged process. Instead, .NET provides a managed abstraction of the operating-system process in the form of an application domain, or app domain. Because app domains are a logical abstraction, a single unmanaged process can contain more than one app domain (see Figure 8-3). Chapter 10 discusses app domains in detail.

The app domain isn't the innermost execution scope of a .NET component. .NET provides a level of indirection between components and app domains, in the form of contexts. Contexts enable .NET to provide component services such as thread synchronization to a component. In fact, one definition of a context is that it's a logical grouping of components, all configured to use the same set of component services.

Figure 8-3. NET app domains and contexts


Every app domain starts with one context, called the default context, and .NET creates new contexts as required.

By default, .NET components aren't aware that contexts exist. When a client in the app domain creates an object, .NET gives back to the client a direct reference to the new object. Such objects always execute in the context of the calling client. However, in order to take advantage of .NET component services, components must be context-bound, meaning they must always execute in the same context. Such components must derive directly or indirectly from the class ContextBoundObject:

     public class MyClass : ContextBoundObject     {...}

Clients never have a direct reference to a context-bound object; instead, they have a reference to a proxy. .NET provides its component services by intercepting the calls clients make into the context via the proxy and performing some pre- and post-call processing. Chapter 11 discusses contexts in depth. In the context of this chapter (no pun intended), all you need to know is that you can have .NET synchronize access to any context-bound component by decorating the component class definition with the Synchronization attribute, defined in the System.Runtime.Remoting.Contexts namespace:

     using System.Runtime.Remoting.Contexts;     [Synchronization]     public class MyClass : ContextBoundObject     {        public MyClass(  )        {}        public void DoSomething(  )        {}        //Other methods and data members     }

The Synchronization attribute, when applied on a ContextBoundObject-based class, instructs .NET to place the object in a context and associate the object with a lock. When a client on thread T1 attempts to access the object by calling a method on it (or accessing a public member variable), the client actually interacts with a proxy. .NET intercepts the client access and tries to acquire the lock associated with the object. If the lock isn't currently owned by another thread, .NET acquires the lock and proceeds to access the object on thread T1. When the call returns from the method, .NET releases the lock and returns control to the client. If the object is then accessed by another thread (T2), T1 is blocked until T2 releases the lock. In fact, while the object is being accessed by one thread, all other threads are placed in a queue and are granted access to the object one at a time, in order. The result of this context-bound synchronization is that .NET provides a macro lock for the object, meaning that object as a wholeeven parts of the state that aren't being accessed by the clientis locked during access.

8.5.1. Synchronization Domains

.NET could have allocated one lock for each context-bound object, but that would be inefficient. If the components are all designed to participate in the same activity on behalf of a client and execute on the same thread, objects can share a lock. In such situations, allocating one lock per object would be a waste of resources and processing time, because .NET would have to perform additional locks and unlocks on every object access. Moreover, the noteworthy argument in favor of sharing locks among objects is that sharing locks reduces the likelihood of deadlocks. If two objects interact with each other and each has its own lock, it's possible for the two objects to be used by two different clients on different threads, so they will deadlock trying to access each other. However, if the objects share a lock, only one client thread is allowed to access them at a time.

In .NET, a set of context-bound objects that share a lock are said to be in a synchronization domain. Each synchronization domain has one lock, and within the same synchronization domain, concurrent calls from multiple threads aren't possible. When a thread accesses one object in a synchronization domain, that thread (and only that thread) can also access the rest of the objects in the synchronization domain. The synchronization domain locks all objects in the domain from access by other threads, even though the current thread accesses only one object at a time.

8.5.2. Synchronization Domains and Contexts

A synchronization domain is independent of contexts, and it can include objects from multiple contexts. However, the synchronization domain is limited to a single app domain, which means that objects from different app domains can't share a synchronization domain lock. A context can belong to at most one synchronization domain at any given time, and may not belong to one at all. If a context belongs to a synchronization domain, all objects in that context belong to that synchronization domain. The reason is that if objects differ in the way they are configured to use synchronization domains, .NET puts them in different contexts in the first place. The relationship between app domains, synchronization domains, contexts, and objects is shown in Figure 8-4.

Figure 8-4. A synchronization domain


8.5.3. Configuring Synchronization Domains

It's up to you to decide how a component is associated with a synchronization-domain lock: you need to decide whether the object needs a lock at all, and if so whether it can share a lock with other objects or requires a new lock. The SynchronizationAttribute class provides a number of overloaded constructors:

     public class SynchronizationAttribute :  ContextAttribute, IContextAttribute                                              //Other interfaces     {        public static const int NOT_SUPPORTED = 1;        public static const int SUPPORTED     = 2;        public static const int REQUIRED      = 4;        public static const int REQUIRES_NEW  = 8;        // Constructors        public SynchronizationAttribute(  );        public SynchronizationAttribute(int flag);        public SynchronizationAttribute(int flag, bool reentrant);        public SynchronizationAttribute(bool reentrant);        //Other methods and properties     }

Four integer constants are defined: NOT_SUPPORTED, SUPPORTED, REQUIRED, and REQUIRES_NEW. These constants determine which synchronization domain the object will reside in relation to its creating client. For example:

     [Synchronization(SynchronizationAttribute.REQUIRES_NEW)]     public class MyClass : ContextBoundObject     {}

The default constructor of the SynchronizationAttribute class uses REQUIRED, and so do the other constructors that don't require a value for the constant. As a result, these declarations are equivalent:

     [Synchronization]     [Synchronization(SynchronizationAttribute.REQUIRED)]     [Synchronization(SynchronizationAttribute.REQUIRED,false)]     [Synchronization(false)]

An object can reside in any of these synchronization domains:

  • In its creator's synchronization domain (the object shares a lock with its creator)

  • In a new synchronization domain (the object has its own lock and starts a new synchronization domain)

  • In no synchronization domain at all (there is no lock, so concurrent access is allowed)

An object's synchronization domain is determined at the time of its creation, based on the synchronization domain of its creator and the constant value provided to the Synchronization attribute. If the object is configured with synchronization NOT_SUPPORTED, it will never be part of a synchronization domain, regardless of whether or not its creator has a synchronization domain. If the object is configured with SUPPORTED and its creator has a synchronization domain, .NET places the object in its creator's synchronization domain. If the creating object doesn't have a synchronization domain, the newly created object will not have a synchronization domain. If the object is configured with synchronization support set to REQUIRED, .NET puts it in its creator's synchronization domain if the creating object has one. If the creating object doesn't have a synchronization domain and the object is configured to require synchronization, .NET creates a new synchronization domain for the object. Finally, if the object is configured with synchronization support set to REQUIRES_NEW, .NET creates a new synchronization domain for it, regardless of whether its creator has a synchronization domain or not. The .NET synchronization domain allocation decision matrix is summarized in Table 8-1.

Table 8-1. Synchronization domain (SD) allocation decision matrix

Object SD support

Creator has an SD

Object will take part in

NOT_SUPPORTED

No

No SD

SUPPORTED

No

No SD

REQUIRED

No

New SD

REQUIRES_NEW

No

New SD

NOT_SUPPORTED

Yes

No SD

SUPPORTED

Yes

Creator's SD

REQUIRED

Yes

Creator's SD

REQUIRES_NEW

Yes

New SD


Figure 8-5 shows an example of synchronization domain flow. In the figure, a client that doesn't have a synchronization domain creates an object configured with synchronization set to REQUIRED. Because the object requires a synchronization domain and its creator has none, .NET creates a new synchronization domain for it. The object then goes on to create four more objects. Two of them, configured with synchronization REQUIRED and SUPPORTED, are placed within the creating object's synchronization domain. The component configured with synchronization NOT_SUPPORTED has no synchronization domain. The last component is configured with synchronization set to REQUIRES_NEW, so .NET creates a new synchronization domain for it. You may be wondering why .NET partly bases the synchronization-domain decision on the object's creating client. The heuristic .NET uses is that the calling patterns, interactions, and synchronization needs between objects usually closely match their creation relationship. The question now is, when should you use the various Synchronization attribute construction values?

Figure 8-5. Synchronization domain flow


The designer of the SynchronizationAttribute class didn't follow basic type-safety practices, and provided the synchronization value in the form of constants instead of an enumeration. As a result, this usage of SynchronizationAttribute:

     [Synchronization(1234)]     public class MyClass : ContextBoundObject     {}

compiles but throws an ArgumentException at runtime. If an enum such as this:

     public enum SynchronizationOption     {         NotSupported,Supported,Required,RequiresNew     }

had been defined, the result would have been type-safe.


8.5.3.1 Synchronization NOT_SUPPORTED

An object set to NOT_SUPPORTED never participates in a synchronization domain. The object must provide its own synchronization mechanism. You should use this setting only if you expect concurrent access and you want to provide your own synchronization mechanisms. In general, avoid this setting. A context-bound object should take advantage of component-services support as much as possible.

8.5.3.2 Synchronization SUPPORTED

An object set to SUPPORTED will share its creator's synchronization domain if it has one and will have no synchronization domain of its own if the creator doesn't have one. This is the least useful setting, because the object must provide its own synchronization mechanism in case its creator doesn't have a synchronization domain, and you must make sure that the mechanism doesn't interfere with the synchronization domain when one is used. As a result, it's more difficult to develop the component. SUPPORTED is available for the rare case in which the component itself has no need for synchronization, yet downstream objects it creates do require it. By setting synchronization to SUPPORTED, the component can propagate the synchronization domain of its creating client to the downstream objects and have them all share one synchronization domain, instead of a few separate synchronization domains. This reduces the likelihood of deadlocks.

8.5.3.3 Synchronization REQUIRED

REQUIRED is by far the most common value for .NET context-bound objects, and hence it's also the default of the SynchronizationAttribute class. When an object is set to REQUIRED, all calls to the object will be synchronized; the only question is whether the object will have its own synchronization domain or share its creator's synchronization domain. If you don't care about the object having its own synchronization domain, always use this setting.

8.5.3.4 Synchronization REQUIRES_NEW

When an object is set to REQUIRES NEW, the object must have a new synchronization domain, distinct from the creator's synchronization domain, and its own lock. The object will never share its context or the synchronization domain with its creator.

8.5.3.5 Choosing between REQUIRED and REQUIRES_NEW

Deciding that your object requires synchronization is usually straightforward. If you anticipate that multiple clients on multiple threads will try to access your object, and you don't want to write your own synchronization mechanism, you need synchronization. The more difficult question to answer is whether your object should require its own synchronization lock or whether you should configure it to use the lock of its creator. Try basing your decision on the pattern of calls to your object.

Consider the calling pattern in Figure 8-6. In this pattern, Object 2 is configured with synchronization set to REQUIRED and is placed in the same synchronization domain as its creator, Object 1. Although the two objects share a lock, they do not interact with each other. While Client A is accessing Object 1, Client B comes along on another thread, wanting to call methods on Object 2. Client B could safely access Object 2, because it doesn't violate the synchronization requirement for the creating object (Object 1). However, because it uses a different thread than Client A, it's blocked and forced to wait until Client A is finished.

Figure 8-6. A calling pattern


If, on the other hand, you configure Object 2 to require its own synchronization domain using REQUIRES_NEW, the object will be able to process calls from other clients at the same time as Object 1 (see Figure 8-7). However, if the creator object (Object 1) does need to call Object 2, these calls will potentially block, and they will be more expensive because they must cross context boundaries and pay the overhead of trying to acquire the lock.

Figure 8-7. Having separate synchronization domains enables clients to be served more efficiently


A classic example of configuring components to require new synchronization domains is when class factories are used to create objects. Class factories usually require thread safety, because they service multiple clients. However, once the factory creates an object, it hands the object back to a client and has nothing more to do with it. You definitely don't want all the objects created by a class factory to share the same synchronization domain as the factory, because no one could use them while the factory creates new objects. In that case, configure the created objects to require new synchronization domains.

8.5.4. Synchronization-Domain Reentrancy

A synchronization domain allows only one thread to enter and locks all the objects in the domain. .NET releases the lock automatically when the thread winds its way back up the call chain, leaving the synchronization domain from the same object through which it entered. Figure 8-8 demonstrates this point: a thread enters a synchronization domain, acquires the lock, and releases it only when it returns from Object 1, leaving the synchronization domain. But what should .NET do if an object makes a call outside the synchronization domain while the call is still in progress inside the domain? This situation is shown in Figure 8-9.

Figure 8-8. Releasing a lock when the call chain exits on the original entry object


Figure 8-9. If reentrancy is set to true, NET releases the synchronization domain lock while the outgoing call is in progress


By default, .NET doesn't release the synchronization-domain lock when the thread exits the domain through a route other than the entry object, such as Object 4 in Figure 8-9. However, because the thread can spend an indefinite amount of time on the outgoing call, it can indefinitely deny other threads access to the synchronization-domain objects. If you want to allow other threads access to the synchronization-domain objects while the current thread makes a call outside the synchronization domain, you need to provide a true value for the reentrant parameter of the SynchronizationAttribute class constructors:

     [Synchronization(true)]     public class MyClass : ContextBoundObject     {}

Be aware that synchronization-domain reentrancy isn't implemented. As a result, the mechanism behaves as if you always pass in false for reentrancy.


In Figure 8-9, if Object 4 is configured to allow reentrancy, when it makes a call outside the synchronization domain .NET releases the lock and allows other threads to enter the domain. Note that when the thread that made the outbound call tries to reenter the synchronization domain it must reacquire the lock, just like any other thread calling from outside the domain. Reentrancy introduces coupling between the objects in the synchronization domain, because the original entry object (Object 1 in Figure 8-9) must also be set for reentrancy, to release the lock when the call chain exits via it.

Allowing for reentrancy is an optimization technique that can increase your application performance and throughput, at the expense of thread safety. In general, I recommend not allowing for reentrancy unless you are convinced that the outgoing call leaves the synchronization domain in a consistent, thread-safe statethat is, that the outgoing thread has no more interactions with the objects in the synchronization domain (apart from returning and winding up the call chain) and that it will exit via the original entry object.

In general, it's a bad design decision to access objects outside your synchronization domain, regardless of whether you can still make things work using the reentrant parameter. If you need to access another object, by design, that object should be part of your synchronization domain. Only clients in non-synchronized contexts should make cross-synchronization domain calls.


8.5.5. Synchronization Domain Pros and Cons

Automatic synchronization for context-bound objects via synchronization domain is by far the easiest synchronization mechanism available to .NET developers. It's a modern synchronization technique that formally eliminates synchronization problems and your need to code around them and test your handcrafted solutions. The resulting productivity gain is substantial. Synchronization domains offer an additional major benefit, too. As explained in Chapter 1, component concurrency management is a core principle of component-oriented programming. A component vendor can't assume that multiple concurrent client threads will not access their components. As a result, unless the vendor states that its components aren't thread-safe, the vendor must provide thread-safe components. Without a way of sharing a lock with the clients, there would always be a synchronization boundary between the vendor server components and the client components. The result would impede performance, because incoming calls would have to negotiate both client-side and server-side locks. More importantly, it would also increase the probability of deadlocks, in the case of different client components competing to use different server components. The ability to share a lock between components developed by different parties is an important feature of .NET as a component technology, analogous to COM's apartments but without the cumbersome model and accompanying liabilities.

Unfortunately, no technology is perfect, and synchronization domains aren't without flaws:

  • You can use them only with context-bound objects. For all other .NET types, you must still use manual synchronization objects.

  • There is a penalty for accessing context-bound objects via proxies and interceptors. In some intense calling patterns, this can pose a problem.

  • A synchronization domain doesn't protect static class members and static methods. For those, you must use manual synchronization objects.

  • A synchronization domain isn't a throughput-oriented mechanism. The incoming thread locks a whole set of objects, even if it interacts with only one of them. That lock precludes other threads from accessing these objects, which can degrade your application throughput.

Caveats aside, I believe that relying on advanced component services (such as synchronization) is a necessity in almost any decent-sized application (or whenever productivity and quality are a top priority), and that the benefits automatic synchronization offers outweigh the drawbacks of using context-bound objects.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net