Dec 19 2007

Locking/Thread Synchronization Performance Question

Category: .NetRory Primrose @ 07:48

Here is my scenario:

I have a static method which checks some static generic dictionaries to determine whether keys exist. If keys don't exist, they will get added along with a value.

Rough "metrics" will be:

  • the static method will get called a lot
  • items will rarely get added to the dictionary
  • items are never removed from the dictionary
  • the dictionary will only contain a few items
  • as many times as the method is called, the dictionary will be referenced using the ContainsKey property (and the indexer property where ContainsKey == true)

In order to ensure thread safety of adding a new item to the dictionary, a lock is required. Two options I see are as follows:

#1 - This is what people would normally do 

lock (MyDictionary)
{
    // Check if a value needs to be stored
    if (MyDictionary.ContainsKey(myKey) == false)
    {
        // Store the value
        MyDictionary.Add(myKey, myValue);
    }
}

#2 - I think this would perform better 

// Check if a value needs to be stored
if (MyDictionary.ContainsKey(myKey) == false)
{
    lock (MyDictionary)
    {
        // Check if a value needs to be stored
        if (MyDictionary.ContainsKey(myKey) == false)
        {
            // Store the value
            MyDictionary.Add(myKey, myValue);
        }
    }
}

The reason I think the second solution will perform better is that the first if statement gets executed as part of the normal code flow of the method. This means lots of executions on multiple threads. It will first check that an item needs to be added before causing the lock. This results in the lock only happening in rare cases. Inside the lock, it ensures that another thread isn't about to do the same thing by making a defensive call to ContainsKey.

In contrast, the first solution will lock out all other threads from checking to see if an item needs to be added causing a major bottleneck.

Keeping in mind the metrics outlined above, is this a good solution? It seems like the best trade-off between thread safety and avoiding bottlenecks with the sacrifice being another call to ContainsKey.

Thoughts?

Tags: ,

Comments

1.
Mike Mike says:

Unfortunately, if you want thread safety, you'll have to stick to #1. Enumerating through a dictionary (and ContainsKey performs an enumeration) is not thread safe.  If you went with #2, you'll eventually find that you're adding a new element while some other thread is in the middle of the unlocked lookup.
In the scenario that you described, you'd probably want to use a ReaderWriterLock rather than the Monitor that the lock() statement uses. That way, all of the reading threads can run simultaneously.

2.
Rory Primrose Rory Primrose says:

I did consider this, however after looking that the Dictionary implementation in Reflector I am not absolutely convinced. The FindEntry method (called by ContainsKey) uses a for loop on an index value rather than an enumerator. I thought that because I only ever add items and never remove them, this shouldn't cause a risk of an out-of-bounds exception being thrown if an item is added when another thread is looping through the index values.

I like the ReaderWriterLock and it seems like a great choice. Someone at work mentioned that using it might actually decrease performance because of the large number of thread context switching. It does look like a nice way of doing it rather than using exclusive locks all the time.

After running some load tests, the results were interesting. Each load test resulted in around 28,500 test executions. The following are the average execution times for the unit test in a load unit test for the given locking scenarios:

No lock - 0.000066
lock statement on write only - 0.000047
lock statement on read and write - 0.00031
ReaderWriterLock on read and write - 0.00011
ReaderWriterLockSlim on read and write - 0.00033

What is intersting about the results is that the using a write only lock was faster than no lock, using a ReaderWriterLock was faster than ReaderWriterLockSlim and that a lock statement on write only was slower than lock statements on on read and write. These are completely the wrong way around from what was expected.

The load test was done on the local machine so I guess the results should be taken with a grain of salt. Each execution time is very small and there could easily have been factors external to the load test that influenced the results.

3.
Rory Primrose Rory Primrose says:

After looking through the results, I noticed a major discrepency with the test of the ReaderWriterLock do to with memory usage on the local machine. After running the load test again, the average execution time was 0.00072.

I have found that because all of the execution times are so small, any differences of results under a millisecond shouldn't be used as a justification for using one method over another because of the external factors in play.

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading