5
\$\begingroup\$

I want to realize an object cache which holds objects loaded from database, file or other slow access source, to make them available quickly with high performance. There will be a lot of accesses to get these objects from the dictionary!

It's not supposed to be Singleton/Static; multiple different object holders are possible, with multiple threads running on each.

Access is usually read-only, the objects themselves (in the dictionary) shall not be changed after being created.

For some special Admin functions, a Reload method exists, where thread safety can be ignored (not to be used by the main, multi-threaded program).

If I am right, the .NET Dictionary is thread-safe, as long as it is used only in read access, thus not requiring locks in reading methods. The dictionary will not be changed after being assigned to the _progObjects variable (do I need to make it volatile?). But time consuming volatile (alternatively Interlocked) access on the _areProgObjectsInitialized variable seems to be necessary, to make first accesses (with initialization) thread-safe.

Is the following a good and performant solution, can it still be improved, or are there safety risks?

/// <summary>
/// Not Singleton, but each instance multi-threaded!
/// Holds objects loaded from an external source (database, file, whatever...)
/// </summary>
public class ProgObjectsThreadSafeRead : IProgObjectsThreadSafeRead
{
    private IDictionary<ObjectKey, object> _progObjects = null;
    private object _progObjectsSyncObj = new object();
    private volatile bool _areProgObjectsInitialized = false;

    private void InitializeProgObjectsIfNeeded()
    {
        if (!_areProgObjectsInitialized)
        {
            lock(_progObjectsSyncObj)
            {
                if (!_areProgObjectsInitialized)
                {
                    IDictionary<ObjectKey, object> localProgObj = LoadProgObjects();
                    // do something more
                    _progObjects = localProgObj;
                    _areProgObjectsInitialized = true;
                }
            }
        }
    }

    public object GetProgObject(ObjectKey key)
    {
        InitializeProgObjectsIfNeeded();
        return _progObjects[key];
    }

    /// <summary>
    /// Not thread safe, don't use in multi-thread environments!
    /// Specific editing tools only! Not in interface.
    /// </summary>
    public void ReloadProgObjects()
    {
        lock (_progObjectsSyncObj)
        {
            _areProgObjectsInitialized = false;
            InitializeProgObjectsIfNeeded();
        }
    }


    // others removed
}
\$\endgroup\$
1
  • \$\begingroup\$ If you want to test the performance of the ReaderWriterLockSlim, here is a small caching class that I did a while ago that uses it : Tiny ThreadSafe Cache C# \$\endgroup\$ Commented May 23, 2012 at 18:43

4 Answers 4

7
\$\begingroup\$

the .NET Dictionary is thread-safe, as long as it is used only in read access, thus not requiring locks in reading methods.

You are playing with fire here. Your statement is only true iff no thread will modify the dictionary. If any of them do, which is about impossible to avoid in a cache, then a lock on both the reading and the writing code is required. A natural consequence of not being able to reliably read a data structure while it is in the process of being modified.

The ReaderWriterLock/Slim classes were designed to provide you this kind of locking. You must acquire a reader lock when you read, a writer lock when you write. Multiple threads can acquire a reader lock so you won't have any serious overhead as long as the cache is productive. The Slim version uses a low cost locking primitive which can reduce the lock cost somewhat. But it is no cure when there's a lot of write contention, no magic formula exists for that. If you do have a lot of write contention then double-check if the old ReaderWriterLock may give better throughput.

And be sure to checkout the .NET 4 MemoryCache class.

\$\endgroup\$
3
  • \$\begingroup\$ In fact, the cache is designed to read once, provide "for all time". The ReloadProgObjects() is only for specific tools not running multi-threaded (I could also mark it "internal") - it is NOT a refreshing method running from time to time again! So my idea was I have only the initialization to synchronize and make sure no thread accesses an uninitialized dictionary. I don't know of any risk that hidden changes to the dictionary might occur from various threads. \$\endgroup\$
    – Erik Hart
    Commented May 20, 2012 at 16:10
  • \$\begingroup\$ Sorry, I can't envision the usefulness of a "read once cache". \$\endgroup\$
    – Hans Passant
    Commented May 20, 2012 at 16:13
  • \$\begingroup\$ His solution looks fine to me. He is saying that he "reads" in the data once and initializes the dictionary and then makes no further changes afterwards. \$\endgroup\$ Commented Jul 28, 2018 at 14:59
3
\$\begingroup\$

If you want a name-value collection that supports concurrent access take a look at System.Collections.Concurrent.ConcurrentDictionary. This does its own internal light weight locking.

NB. it is not suitable if you need to work with multiple data structures together or where multiple operations need to be performed on the dictionary (a different thread might get access between operations that should always happen together). For independent operations (separate reads and writes/updates) a ConcurrentDictionary is thread safe.

\$\endgroup\$
1
\$\begingroup\$

You were right about the dictionary, the only thing i would change (i don't know if it is relevant for you, but it is a classic usage in your case) is using ReaderWriterLockSlim - http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim(v=vs.100).aspx

\$\endgroup\$
2
  • \$\begingroup\$ As I read, even the new ReaderWriterLockSlim is still by factor 1.7 slower than a classic lock (aka Monitor) block, when entering in read mode. My goal was to avoid locks for reading, in best case to abandon all synchronization after the object is initialized, but it seems to be impossible or dangerous - so I resort to primitive sync access (volatile, interlocked). ReaderWriterLocks may be good for more complex accesses where multiple readers outweight the ReaderWriterLock penalty, but probably not for getting values from a dictionary. \$\endgroup\$
    – Erik Hart
    Commented May 20, 2012 at 14:30
  • \$\begingroup\$ It depends on your usage: stackoverflow.com/questions/407238/… Anyway, i was wrong because i meant ReaderWriterLockSlim and not ReaderWriterLock \$\endgroup\$
    – eyossi
    Commented May 20, 2012 at 14:55
0
\$\begingroup\$

I know this is late but your solution looks fine. You are creating the dictionary once and then never changing it afterwards so you can safely read from it from as many threads as you like. You don't need the volatile keyword - it will cause calls to your GetProgObject method to be slower under normal circumstances with no benefit since the lock will ensure that the latest value is read within your lock statement. Even if the old value is cached on another processor core, it will very quickly update to the latest correct value within the lock statement.

\$\endgroup\$

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.