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.

Jeffrey Richter

View Comments

  • Did you have to take any special care to guard against app domain agile objects causing any kind of false sharing or other such leaks in the abstraction? (Say something silly like taking a lock on an interned string?)

  • So, your solution is kind of TLS, right ? If so, wouldn't that break some of their business logic because for non-thread code, they could see all of the items ???

  • p90xkicks
    P90x
    cheap p90x dvd
    P90x workout sale
    p90x dvd sale
    cheap P90x workout
    discount p90x dvd
    p90x online sale
    [url=http://www.p90xkicks.com][b]p90xkicks[/b][/url]
    [url=http://www.p90xkicks.com][b]P90x [/b][/url]
    [url=http://www.p90xkicks.com][b]cheap p90x dvd [/b][/url]
    [url=http://www.p90xkicks.com][b]P90x workout sale [/b][/url]
    [url=http://www.p90xkicks.com][b]p90x dvd sale[/b][/url]
    [url=http://www.p90xkicks.com][b]cheap P90x workout [/b][/url]
    [url=http://www.p90xkicks.com][b]discount p90x dvd [/b][/url]
    [url=http://www.p90xkicks.com][b]p90x online sale[/b][/url]

  • @Jeffrey - Since the customer did not want to modify their code-base and want to achieve thread safety, couldn't we use "lock" (or Monitor) on a static read-only object (since static read-only is thread-safe)?
    E.g.
    public static class NonCurrentAlgorithmProxy
    {
    private static readonly object _myLock = new Object();
    public static Add(string item)
    {
    lock(_myLock)
    {
    NonCurrentAlgorithm.Add(Item);
    }
    }
    ...
    }
    May look rudimentary, but I believe it should work. What do you think?

  • I submitted the comments earlier but not sure if it got posted.
    The soluction proposed is like TLS. But my question is that the business logic may expect that to be a 'global' list. What I am saying is that if the current non-threading mode has three tasks executed sequentially and the three (final) thread may expect the 1st 2 tasks inserted into the same list. So, the final task is to process the list. So, if I make thread1 and thread2 appdomain to make the process parallel (i.e. faster) and my task3 will not be processing correctly ? Or am I missing something

  • Could you have marked all static fields with the ThreadStatic attribute to achieve the same result? It looks like it would work for s_items in your example.

Recent Posts

8-Step AWS to Microsoft Azure Migration Strategy

Microsoft Azure and Amazon Web Services (AWS) are two of the most popular cloud platforms.…

5 days ago

How to Navigate Azure Governance

 Cloud management is difficult to do manually, especially if you work with multiple cloud…

2 weeks ago

Why Azure’s Scalability is Your Key to Business Growth & Efficiency

Azure’s scalable infrastructure is often cited as one of the primary reasons why it's the…

4 weeks ago

Unlocking the Power of AI in your Software Development Life Cycle (SDLC)

https://www.youtube.com/watch?v=wDzCN0d8SeA Watch our "Unlocking the Power of AI in your Software Development Life Cycle (SDLC)"…

1 month ago

The Role of FinOps in Accelerating Business Innovation

FinOps is a strategic approach to managing cloud costs. It combines financial management best practices…

1 month ago

Azure Kubernetes Security Best Practices

Using Kubernetes with Azure combines the power of Kubernetes container orchestration and the cloud capabilities…

2 months ago