Using AppDomains to make Non-Threadsafe code Threadsafe

Recently, I was involved in a Wintellect consulting engagement where a customer had some class library code that was created years ago. The code in this class library was not designed to work well in a multithreaded environment. Specifically, this means that if two or more threads called into the class library code at the same time, the data got corrupted and the results could not be trusted. Non-concurrent-ready code is easy to obtain with frequent use of mutable static fields. But, there are other common coding practices which can result in code that is not safe with multiple threads passing through it at the same time.

This customer wanted to increase the performance of their code and the best way to accomplish this is to have multiple threads processing their own data independently of each other. But, again, the class library code base would produce errant results if we did this. To demonstrate the problem, imagine this very simple static class which is indicative of the non-thread safe class library that I’m referring to:

internal
static
class
NonConcurrentAlgorithm {


private
static
List<String> s_items = new
List<String>();


public
static
void Add(String item) { s_items.Add(item); }


public
static
String[] GetItems() { return s_items.ToArray(); }

}

If we have two threads using this class simultaneously, the results are unpredictable. For example, let’s say we have this code:

ThreadPool.QueueUserWorkItem(o => NonConcurrentAlgorithm.Add("Item 1"), null);
NonConcurrentAlgorithm.Add("Item 2");
Thread.Sleep(1000);  // To demonstrate the problem consistently
foreach (var i in NonConcurrentAlgorithm.GetItems())
   Console.WriteLine(i);

When this code runs, the console could show:

  • “Item 2” by itself
  • “Item 2” followed by “Item 1”
  • “Item 1” followed by “Item 2”

Clearly, this is not desireable and the problem all stems from the threads sharing the one List<String> object. In fact, the List<T> class is not itself thread-safe and so it is even possible that having multiple threads accessing it simultaneously could result in items disappearing or a single item being inserted multiple times, or worse. It depends on how the List<T> class is internally implemented which is not documented and is subject to change.

Now, one way to improve performance for the customer would be for Wintellect to take the non-concurrent code base and make it thread safe. This is the best thing to do; however, the code base was very large, the changes to the code base would have been substantial and a lot of testing would have had to be done. The customer was not too excited by this recommendation. So, what we did instead was use the CLR’s AppDomain feature. This works out great because an AppDomain’s state is completely isolated from all other AppDomains. To communicate across AppDomains, you must create a class derived from MarshalByRefObject. So, I created a NonConcurrentAlgorithmProxy class that wraps the methods of the static class. The NonConcurrentAlgorithmProxy class looks like this:

internal sealed class NonConcurrentAlgorithmProxy : MarshalByRefObject {
   public void Add(String item) { NonConcurrentAlgorithm.Add(item); }
   public String[] GetItems()   { return NonConcurrentAlgorithm.GetItems(); }
}

Then we modified the code that uses this class library code so that each thread creates its own AppDomain and creates an instance of the NonConcurrentAlgorithmProxy class in each AppDomain. The static field in the NonConcurrentAlgorithm class is now one-per-AppDomain and since each thread gets its own AppDomain, there is now one List<String> object per thread. Here is the new code that uses the class library code via the proxy class:

// Create Thread 1's AppDomain & proxy
var thread1AppDomain = AppDomain.CreateDomain(String.Empty);
var thread1Proxy = (NonConcurrentAlgorithmProxy)
   thread1AppDomain.CreateInstanceAndUnwrap(
      typeof(NonConcurrentAlgorithmProxy).Assembly.FullName,
      typeof(NonConcurrentAlgorithmProxy).FullName);
                    
// Create Thread 2's AppDomain & proxy
var thread2AppDomain = AppDomain.CreateDomain(String.Empty);
var thread2Proxy = (NonConcurrentAlgorithmProxy
   thread2AppDomain.CreateInstanceAndUnwrap(

                    typeof(NonConcurrentAlgorithmProxy).Assembly.FullName, 

                    typeof(NonConcurrentAlgorithmProxy).FullName);
                    
// Have 2 threads execute the same code at the same time but in different AppDomains
ThreadPool.QueueUserWorkItem(o => thread2Proxy.Add("Item 1"), null);
thread1Proxy.Add("Item 2");
Thread.Sleep(1000);  // To demonstrate the problem has been fixed
                    
// Show Thread 1's list of items; guaranteed to be "Item 2"
foreach (var i in thread1Proxy.GetItems())
   Console.WriteLine(i);
                    
// Show Thread 2's list of items; guaranteed to be "Item 1"
foreach (var i in thread2Proxy.GetItems())
   Console.WriteLine(i);
                    
// Cleanup each thread's AppDomain
AppDomain.Unload(thread1AppDomain);
AppDomain.Unload(thread2AppDomain);

This is substantially less coding, testing, and effort than what would have been involved with making the customer’s current code base thread safe! And, this technique can scale very nicely for many threads as well. Of course, there is a slight performance penalty when creating/destroying AppDomains and calling methods through the proxy. This is why it would be ideal to make the existing code base thread safe. But, this is a great compromise and the customer was quite happy with the results and the time/cost it required to improve their application’s performance substantially.

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X