Synchronization Domains

The best place to avoid deadlocks is in the design stage—and that's where synchronization domains come in.


August 01, 2004
URL:http://www.drdobbs.com/windows/synchronization-domains/184405771

August, 2004: Synchronization Domains

How to use .NET synchronization domains

Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at richardrichardgrimes.com.


A blocked thread is a thread that does no work. Moreover, a blocked thread could have exclusive access to a resource and another thread could block waiting to get exclusive access to the same resource. Deadlocks caused by situations like this are notoriously difficult to reproduce and debug. It's far better to design code so that deadlocks do not occur. In this article, I focus on one facility .NET provides that does this—synchronization domains.

COM and Synchronization Domains

Synchronization domains are not new to Windows. COM provided synchronization through apartments. Specifically, the single-threaded apartment (STA) contained a single thread used to access all the objects in the apartment. But the STA is more than just a mechanism to restrict access to a single thread—it is implemented to use a Windows message queue to store calls into the STA so that the requests are synchronized with Windows messages. This means that STA objects running on a GUI thread won't freeze the user interface and that the message queue can be used to provide safe reentrance so that, while an STA object is making a call out of the apartment, another object can make a call into the apartment.

COM users could utilize STA synchronization to manage multiple objects that required access to a shared resource. To do this, you make all of those objects run in the same STA, which by definition means that a single thread handles calls to all of the objects. Since there is only one thread, this means that only one thread at any time can have access to the resource. To use STA synchronization, a COM class has to be marked in the registry with the string value ThreadingModel of Apartment. Inevitably, this is an issue because information about a COM class is stored in a separate location to the class. However, the COM object does not take part in synchronization, it is the COM system's responsibility to ensure that the object is created in an STA, and creates an STA if one does not exist. Furthermore, if an STA object creates another STA object, the COM system ensures that both objects run in the same (single-threaded) apartment.

This synchronization was further extended with Transaction Server (MTS) and COM+, which has a facility called an "activity." In effect, an activity is a logical thread that extends to more than one process or machines. The COM+ system needs to determine if a class should run in an activity and to do this it consults the COM+ catalog for the class's metadata. COM+ metadata is more expressive than COM because it identifies not only whether the class should run in an activity, but it also specifies whether objects of the class can run in an existing activity or if it needs a new activity. However, the COM+ catalog still has the same issue as COM registration: The metadata is stored in a separate location and there's a possibility of a third party changing the metadata to something inappropriate.

.NET Contexts

COM+ is essentially a precursor for .NET. COM+ introduced component services and object context, which provides automatic transaction enlistment and activities but does not let you customize contexts. .NET also provides component services but, in contrast to COM+, the .NET context architecture is extensible.

.NET component services are provided through context attributes on context-bound objects. A context is an execution environment that contains one or more objects that require the same component services. When a context-bound object is created, the runtime checks the context where the activation request was made. If this context provides the services required by the new object, then the object is created in the creator's context. If the creator's context is not suitable, the runtime creates a new context. In general, any object accessing a context-bound object does so through a proxy object and when the proxy is created (the context-bound object is marshaled to the other context), the runtime sets up a chain of "sink" objects that add the component services of the context to a method invocation. When an object is called across a context boundary, the method invocation is converted into a message object that is passed through the chain of sinks.

Component services are only applied on context-bound objects, which means that the class derives (directly or indirectly) from ContextBoundObject. This class derives from MarshalByRefObject and the two classes mean that after the object is created, it always remains in the same context and to access it from another context requires marshaling. A class developer indicates the component services that the class uses by applying an attribute to the class. This means that the metadata is part of the class. Only a developer can change the metadata or the implementation of the class that uses the component services. Class metadata is no longer the fragile data that it was with COM and COM+.

Context Attributes

Context attributes are more than just metadata—they are derived from a class called ContextAttribute, which means that they have the responsibility of determining whether a candidate context has the required component services. The context attribute also has the responsibility of creating the sink object that provides the component service. The runtime calls the attribute and passes the next sink in the chain so that the attribute can create its own sink object and insert it into the chain.

Figure 1 is a simplified schematic of this mechanism. The sink objects are only called when a method call is made across a context boundary. The proxy object looks like the object in the object context and has the responsibility of converting a method call into a message object. In effect, this message is a serialization of the method parameters and information about the method, and it is passed through the sink chain in the client context through a special channel object that makes the cross context call and then through the sink chain in the object's context. Each sink object can handle the method call according to the component service it is providing. Sinks can read the data in the message and can provide processing before passing the message to the next sink, or may even decide to handle the message itself without calling the next sink (for example, if the sink chooses to generate an exception). The final sink in the chain builds the stack from the data in the message and uses this to call the object method.

Synchronization

The runtime provides the [Synchronization] attribute in the System.Runtime.Remoting.Contexts namespace to add synchronization to a context-bound object. This attribute is initialized with two values—one value determines whether the context should be reentrant, and the other value specifies how the synchronization context is created. Access to an object that has synchronization is subject to a lock. A thread attempting to access the object has to obtain the lock and, while one thread possesses the lock, no other thread can have access. A single lock for every object would be rather wasteful but, more worrisome, it presents a possibility of deadlock. Instead, .NET defines synchronization domains where objects share a single lock and only one thread at any time can possess the lock.

The synchronization domain is controlled by the flag parameter of the [Synchronization] constructor. This parameter is an integer even though only four values are acceptable; these four possible values are defined as constant fields as part of the SynchronizationAttribute class. This design is not typesafe and it causes problems if you use Managed C++. The Managed C++ compiler insists that a value used as an argument for an attribute must be evaluated at compile time, and it insists that this is the case only if the field has the special C++ metadata modifier IsConstModifier. This modifier is not used on the fields of SynchronizationAttribute and so the C++ compiler generates the "C2363" error. This problem can be solved by casting the field to Int32 before using it as a parameter to the attribute.

The two most important values that can be passed to the flag parameter are REQUIRED and REQUIRES_NEW. In both cases, the object's context must synchronize access and the values indicate whether the object can be created in an existing synchronized context or require a new context. The synchronized context is generally called a "synchronization domain." This can contain one or more objects that share the same synchronization lock. REQUIRED is the default and indicates that the creator's context can be used if it is a synchronized context. This means that the creator and the new object are in the same synchronization domain and share the same lock. If the creator's context is not synchronized, a new context is created. REQUIRES_NEW indicates that a new context is always created regardless of the context of the creator. This is useful for class factory objects used to create objects. Consequently, it should not need synchronized access to the objects it creates.

Unlike STA synchronization, .NET synchronization domains do not have thread affinity; that is, a synchronization domain does not restrict access to the objects it contains to a single thread. Instead, it restricts access to a single thread at one particular time. That is, once one thread has completed its work within the synchronization domain and has released the lock, another thread can obtain the lock and access the objects. Contrast this to STA synchronization where only one specific thread can access the objects throughout the lifetime of the apartment.

For example, Listing One shows a base class that has a single method Run(). This method extracts the name of the class and prints it 100 times on the console. The console is a shared resource, which means that you should be careful when writing to it with multiple threads. Listing Two shows two classes derived from the base class and a third class that runs those two classes on separate thread pool threads. Base.Run() calls Thread.Sleep() every iteration of the for loop and this has the effect of relinquishing the time slice after the current thread has slept for the specified number of milliseconds. Since there are two threads running concurrently, it means that, when one thread relinquishes its time slice, it lets the other thread run. This means that the code in Listing Two should print a sequence of AB pairs on the console. (The sequence starts with a line of As and ends in a line of Bs because it takes a little time for a pool thread to run the method in response to the second call to QueueUserWorkItem.)

To synchronize the calls to the objects, you should apply the [Synchronization] attribute with REQUIRED. This attribute is inherited and since the two test classes are derived from Base, it makes sense to add the attribute to that class. However, if you rerun the code with this change, the result is the same as before. The reason is because both objects are created in the App.Start() method, which is not running in a synchronized context, so the runtime creates two synchronized contexts, one for each of the objects. The runtime makes no attempt to search for a suitable context, other than checking to see if the creator's context is suitable.

To create a single synchronization domain, you need to create an object in a synchronized context and make this object create the additional object. To do this, the class A has a method that creates and returns the B object—CreateB()—and App.Start() uses this method to create the B object. Listing Three shows these changes.

Implementation of SynchronizationAttribute

It is instructive to dig deeper into the implementation of SynchronizationAttribute, to get a better understanding of how synchronization works and to learn techniques that you can apply elsewhere. You can view the implementation either by reading the IL with ILDASM, decompiling the assembly with something like Anakrino (http://www.saurik.com/net/exemplar/) or Reflector (http://www.aisto.com/roeder/dotnet/). Better yet, you could take a look at the synchronizeddispatch.cs source file in the Shared Source CLI (http://msdn .microsoft.com/net/sscli/). This shows that the client-side sink is created from a class called SynchronizedClientContextSink. Client-side sinks are used when an object in a synchronized context makes a call to an object outside of the context. In this case, the code behaves differently for objects that allow reentrance and those that don't. In the former case, the synchronization lock has to be released to allow another thread to call into the synchronization domain. Then, when the outgoing call returns, the thread has to wait to obtain the synchronization lock. If the code is not reentrant, it means that threads cannot execute code in the domain while the outgoing call is active and so the thread does not have to release the lock.

The object-side sink is called SynchronizedServerContextSink and it handles the situation when a call from outside the context is made on the object. In this example, the thread-pool thread would be executing in a different context and so the SynchronizedServerContextSink methods are invoked when this thread accesses instances of either A or B. It is important to note that both of these sink objects hold a reference to the attribute that created them. This means that the client and object contexts must be in the same application domain.

The synchronization process relies on two main objects: a Queue (a first-in/first-out queue) to hold information about the method request and a Monitor to perform interthread communication. The synchronization works like this: First, a call comes into the context and the code checks to see if any requests are queued. If there are no queued method invocation requests, the current thread gains the synchronization lock by setting an internal flag to True and the current method invocation request is passed to the object. If a second request is made while another request is being handled, a check on the synchronization flag will indicate that the domain is locked and so the request is put into the queue as a WorkItem object. This second thread is blocked. To do this, Monitor.Wait() is called on the WorkItem object that was put in the queue. Meanwhile, the first thread continues its work and, when it has completed, it extracts the next WorkItem from the queue and passes it to Monitor.Pulse(), which wakes up the waiting thread. This woken thread will be the only thread running in the synchronization domain, so it automatically gets the lock. This thread can then execute its work and if any other threads try to get access to any object in the domain, those method calls are queued until the current thread has completed its work. When the last thread has completed its work, the synchronization flag is cleared and ready for the next batch of calls that may happen sometime later.

There are two important points to note here:

.NET and COM+ Activities

.NET still gives access to COM+ activities and, rather confusingly, it also uses an attribute called [Synchronization] in the System.EnterpriseServices namespace to indicate that an object will take part in an activity. However, this attribute is significantly different in meaning and implementation to the native synchronization attribute I have just described. [Synchronization] also takes a parameter to indicate whether the object can join an existing activity or must have its own activity: however, this time the parameter is an enumeration that makes it typesafe.

COM+ objects must run in a context to be able to benefit from COM+ component services, and it does this by deriving from ServicedComponent. However, the COM+ [Synchronization] is not a context attribute and it is not .NET that applies the synchronization. Instead, when the serviced component is registered with COM+, the regsvcs tool uses reflection to read information about the serviced components in an assembly. The [Synchronization] attribute on a class merely means that the regsvcs tool should add the synchronization metadata to the class's entry in the COM+ catalog. Again, the catalog is consulted by COM+ when it activates objects, and the COM+ runtime then creates and manages activities if they are required.

Registering an assembly with the COM+ catalog adds extra overhead to your application deployment and there is runtime overhead because the object is running under COM+ (although this overhead will be minimized if the .NET code is configured to be a COM+ library). If you want to have a synchronization domain within a process, then the .NET synchronization domain should be used in preference to a COM+ activity. However, if you want to synchronize objects that use process isolation (rather than the lightweight .NET application domain isolation) or run on different machines, then you can only use a COM+ activity.

DDJ



Listing One

class Base : ContextBoundObject
{
   // Write the class name 100 times on the console
   public void Run(object o)
   {
      string str = GetType().ToString();
      for (int i = 0; i < 100; i++)
      {
         Console.Write(str);
         Thread.Sleep(20);
      }
      Console.WriteLine();
   }
}
Back to article


Listing Two
class A : Base {}
class B : Base {}

class App
{
   static void Main()
   {
      App app = new App();
      app.Start();
      // Keep main thread alive
      Console.ReadLine();
   }
   void Start()
   {
      A a = new A();
      B b = new B();
      ThreadPool.QueueUserWorkItem(new WaitCallback(a.Run));
      ThreadPool.QueueUserWorkItem(new WaitCallback(b.Run));
   }
}
Back to article


Listing Three
[Synchronization(SynchronizationAttribute.REQUIRED)]
class Base : ContextBoundObject
{
   // rest of the code is the same as before
}
class A : Base 
{
   // Create a B object in the same context as the A object
   public B CreateB()
   {
      return new B();
   }
}
class B : Base {}
class App
{
   static void Main()
   {
      App app = new App();
      app.Start();
      // Keep main thread alive
      Console.ReadLine();
   }
   void Start()
   {
      A a = new A();
      B b = a.CreateB();
      ThreadPool.QueueUserWorkItem(new WaitCallback(a.Run));
      ThreadPool.QueueUserWorkItem(new WaitCallback(b.Run));
   }
}
Back to article

August, 2004: Synchronization Domains

Figure 1: Schematic showing calls between context-bound objects.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.