4
\$\begingroup\$

I have a class extending the Dictionary class. This class is used for storing some information (modeled in CustomClass) and accessing it through an integer ID.

To extend this class I have added a TryAdd method, specific to my workflow, which implement different behaviours for the cases of trying to add a new or a already existing CustomClass object.

public class CustomDictionary : Dictionary<int, CustomClass>
    {
        private void TryAdd(int ID, CustomClass customObject)
        {
            if (this.ContainsKey(ID))
            {
                //some operations
            }
            else
            {
                //some others operations
                this.Add(customObject);
            }
        }
    }

There will be only one instance object for this class. I want to add locks to provide thread safety and syncronisation for the object of type CustomDictionary. TryAdd and data accessing operation will frequently occur in parallel for this structure.

Have in mind that at the moment I can't use framework 4.0 so I can't use ConcurrentCollections.

To ensure thread safety I put this.Add(customObject); within a lock(this) but I have read that this is very bad.

Then I read about locking using a private object.

public class CustomDictionary : Dictionary<int, CustomClass>
    {
        private object lockObject = new Object();

        private void TryAdd(int ID, CustomClass customObject)
        {
            if (this.ContainsKey(ID))
            {
                //some operations
            }
            else
            {
                //some others operations
                lock(lockObject)
                {
                    this.Add(customObject);
                }
            }
        }
    }

Is this the good way of doing it? Should I also lock the CustomDictionary object when I read data from that object?

Any improvements for the implementation of this class would be helpfull.

\$\endgroup\$
5
  • 3
    \$\begingroup\$ You should put the lock around all the code in the TryAdd method. If two threads both hit the 'ContainsKey' before an addition is performed, both threads will then add the object. \$\endgroup\$ Sep 25, 2012 at 7:44
  • \$\begingroup\$ I think you'd want to change your code to lock(lockObject) \$\endgroup\$
    – Ron Klein
    Sep 25, 2012 at 12:11
  • \$\begingroup\$ The whole .NET standard Dictionary object is not thread safe, even for read operations. You can't really derive from it and hope it will work. The best is to enclose all actions externally with a lock. Or use .NET 4 of course :-) \$\endgroup\$ Sep 25, 2012 at 15:25
  • \$\begingroup\$ @Rob Klein: the lock(object) was a mistake. \$\endgroup\$
    – Coral Doe
    Sep 26, 2012 at 6:47
  • \$\begingroup\$ @SimonMourier: Using .NET 4 is always suggested, but like I said in the question, it is not an option. I am lobbying for almost a year for .NET 4 with no results; I gotta stick with what I have. \$\endgroup\$
    – Coral Doe
    Sep 26, 2012 at 6:48

1 Answer 1

3
\$\begingroup\$

First of all, it's very good that you read about the evilness of lock(this). Avoid it at all costs :)

As for your solution, it's not thread safe, in terms of functionality:

Thread A tries to add an object to id = 123.
Since this object doesn't exist, the ContainsKey returns false, so the false part of the condition takes place.
Then Thread A acquires the lock and starts to add its object.

However, Thread A hasn't yet completed its processing, and the system now switches to Thread B.

Strangely enough, Thread B wants to add its own object with the same id = 123 (!).

And, what do you know, since Thread A hasn't finished its processing, Thread B asks if the dictionary ContainsKey(123) and gets false. That's the problem.

Even worse, when Thread B starts its processing of adding the object, an exception would be thrown, since there's already a key assigned with 123.

So, no, the suggested code is not thread safe. That's a classic race condition.

How to resolve this?

Option 1: Double check on read operations

Inside the lock, you can call again to the ContainsKey, which is a read operation. MS provided a cheering statement, that read operations on its data structures (dictionary, list, etc.) are all thread safe.

Update: from MSDN: A Dictionary can support multiple readers concurrently, as long as the collection is not modified (my emphasis). So please ignore this option.

Option 2: Use ReaderWriterLockSlim Class

This class is optimized to the scenario of multiple reads / seldom writes. So when you want to write, you upgrade your lock to have "write" scope, and you continue your code.

Option 3: Use a simple lock on the entire TryAdd method. This is very straight forward.

All options are fine, and my personal bias is towards option 2.

Update: see also a related post at Ayende: Why is this not thread safe?

Good luck!

\$\endgroup\$
2
  • \$\begingroup\$ Thank you for your well documentated answer. It really helped me understand this issue and I've implemented the second option. \$\endgroup\$
    – Coral Doe
    Sep 28, 2012 at 9:46
  • \$\begingroup\$ @CoralDoe, my pleasure! \$\endgroup\$
    – Ron Klein
    Sep 28, 2012 at 18:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.