Skip to main content

A simple, thread-safe synchronized C# class to add, update, delete and read items from cache.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

/// <summary>
/// Synchronized Cache (Thread Safe)
///
/// A simple, thread-safe synchronized class to add, update, delete and read
/// items from cache.
///
/// The cache holds strings with integer keys. An instance of
/// ReaderWriterLockSlim is used to synchronize access to the
/// Dictionary&lt;TKey, TValue&gt; that serves as the inner cache (_cache).
///
/// Modified version of SynchronizedCache example from MSDN:
/// https://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim(v=vs.110).aspx
/// </summary>
public class SynchronizedCache
{
    /// <summary>
    /// Uses the default constructor to create the lock, so recursion is
    /// not allowed. Programming the ReaderWriterLockSlim is simpler
    /// and less prone to error when the lock does not allow recursion.
    /// </summary>
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    /// <summary>
    /// The internal cache dictionary instance.
    /// </summary>
    private Dictionary<int, string> _cache = new Dictionary<int, string>();

    /// <summary>
    /// Returns a count of the total number of cached items.
    /// </summary>
    public int Count
    {
        get
        {
            _lock.EnterReadLock();

            int count;
            try
            {
                count = _cache.Count;
            }
            finally
            {
                _lock.ExitReadLock();
            }

            return count;
        }
    }

    /// <summary>
    /// Finds the cached item by key, and returns its value.
    /// </summary>
    /// <param name="key">The cache key.</param>
    /// <returns>The cached item value.</returns>
    public string Read(int key)
    {
        _lock.EnterReadLock();

        string result;
        try
        {
            result = _cache[key];
        }
        finally
        {
            _lock.ExitReadLock();
        }

        return result;
    }

    /// <summary>
    /// Adds an item to be cached.
    /// </summary>
    /// <param name="key">The cache key.</param>
    /// <param name="value">The cache value.</param>
    public void Add(int key, string value)
    {
        _lock.EnterWriteLock();

        try
        {
            _cache.Add(key, value);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    /// <summary>
    /// Adds an item to be cached if it can do so within a specified timeout
    /// period parameter.
    /// </summary>
    /// <param name="key">The cache key.</param>
    /// <param name="value">The cache value.</param>
    /// <param name="timeout">
    /// The total time in milliseconds to wait for the Add operation to complete.
    /// </param>
    /// <returns>
    /// Returns true if the cache item was inserted before the specified
    /// timeout period, otherwise false.
    /// </returns>
    public bool Add(int key, string value, int timeout)
    {
        if (_lock.TryEnterWriteLock(timeout))
        {
            try
            {
                _cache.Add(key, value);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    /// <summary>
    /// Adds or Updates a Cache Item
    ///
    /// The operation will retrieve the value associated with a key and
    /// compares it with a new value. If the value is unchanged, the method
    /// returns a status indicating no change. If no value is found for
    // the key, the key/value pair is inserted. If the value has changed,
    // it is updated.
    ///
    /// </summary>
    /// <param name="key">The cache key.</param>
    /// <param name="value">The cache value.</param>
    /// <returns>The operation AddOrUpdateStatus.</returns>
    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
        // Upgradeable mode allows the thread to upgrade from read access to
        // write access as needed, without the risk of deadlocks.
        _lock.EnterUpgradeableReadLock();

        try
        {
            string result = null;
            if (_cache.TryGetValue(key, out result))
            {
                if (result == value)
                {
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
                    _lock.EnterWriteLock();

                    try
                    {
                        _cache[key] = value;
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }

                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
                _lock.EnterWriteLock();

                try
                {
                    _cache.Add(key, value);
                }
                finally
                {
                    _lock.ExitWriteLock();
                }

                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();
        }
    }

    public void Delete(int key)
    {
        _lock.EnterWriteLock();

        try
        {
            _cache.Remove(key);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public enum AddOrUpdateStatus
    {
        Added,
        Updated,
        Unchanged
    };

    ~SynchronizedCache()
    {
        if (_lock != null)
        {
            _lock.Dispose();
        }
    }
}

//
// ---------------------------------------------------------------------------
// EXAMPLE USAGE
// ---------------------------------------------------------------------------
//

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

public class Example
{
    public static void Main()
    {
        var sc = new SynchronizedCache();
        var tasks = new List<Task>();

        int itemsWritten = 0;

        // Execute a writer:
        tasks.Add(Task.Run( () =>
        {
            string[] vegetables = {
                "broccoli",
                "cauliflower",
                "carrot",
                "sorrel",
                "baby turnip",
                "beet",
                "brussel sprout",
                "cabbage",
                "plantain",
                "spinach",
                "grape leaves",
                "lime leaves",
                "corn",
                "radish",
                "cucumber",
                "raddichio",
                "lima beans"
            };

            for (int ctr = 1; ctr <= vegetables.Length; ctr++)
            {
                sc.Add(ctr, vegetables[ctr - 1]);
            }

            itemsWritten = vegetables.Length;
            Console.WriteLine("Task {0} wrote {1} items\n", Task.CurrentId, itemsWritten);
        }));

        // Execute two readers, one to read from first to last
        // and the second from last to first:
        for (int ctr = 0; ctr <= 1; ctr++)
        {
            bool desc = Convert.ToBoolean(ctr);
            tasks.Add(Task.Run( () =>
            {
                int start, last, step;
                int items;
                do
                {
                    string output = string.Empty;
                    items = sc.Count;
                    if (! desc)
                    {
                        start = 1;
                        step = 1;
                        last = items;
                    }
                    else
                    {
                        start = items;
                        step = -1;
                        last = 1;
                    }

                    for (int index = start; desc ? index >= last : index <= last; index += step)
                    {
                        output += string.Format("[{0}] ", sc.Read(index));
                    }

                    Console.WriteLine("Task {0} read {1} items: {2}\n",
                        Task.CurrentId, items, output);
                }

                while (items < itemsWritten | itemsWritten == 0);
            }));
        }

        // Execute a red/update task:
        tasks.Add(Task.Run(() =>
        {
            Thread.Sleep(100);
            for (int ctr = 1; ctr <= sc.Count; ctr++)
            {
                string value = sc.Read(ctr);
                if (value == "cucumber")
                {
                    if (sc.AddOrUpdate(ctr, "green bean")
                        != SynchronizedCache.AddOrUpdateStatus.Unchanged)
                    {
                        Console.WriteLine("Changed 'cucumber' to 'green bean'");
                    }
                }
            }
        }));

        // Wait for all three tasks to complete:
        Task.WaitAll(tasks.ToArray());

        // Display the final contents of the cache:
        Console.WriteLine();
        Console.WriteLine("Values in synchronized cache: ");
        for (int ctr = 1; ctr <= sc.Count; ctr++)
        {
            Console.WriteLine("   {0}: {1}", ctr, sc.Read(ctr));
        }

        Console.ReadKey();
    }
}