Skip to main content

This recipe will describe how to create a thread-safe mechanism to read and write to a collection from multiple threads using a ReaderWriterLockSlim construct. ReaderWriterLockSlim represents a lock that is used to manage access to a resource, allowing multiple threads for reading or exclusive access for writing.

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

//
// Using the ReaderWriterLockSlim construct
//
// This recipe will describe how to create a thread-safe mechanism to read and
// write to a collection from multiple threads using a ReaderWriterLockSlim
// construct. ReaderWriterLockSlim represents a lock that is used to manage
// access to a resource, allowing multiple threads for reading or exclusive
// access for writing.
//
// How it works...
//
// When the main program starts, it simultaneously runs three threads that read
// data from a dictionary and two threads that write some data into this
// dictionary. To achieve thread safety, we use the ReaderWriterLockSlim
// construct, which was designed especially for such scenarios.
//
// It has two kinds of locks: a read lock that allows multiple threads reading
// and a write lock that blocks every operation from other threads until this
// write lock is released. There is also an interesting scenario when we obtain
// a read lock, read some data from the collection, and depending on that data,
// decide to obtain a write lock and change the collection. If we get the write
// locks at once, too much time is spent not allowing our readers to read the
// data, because the collection is blocked when we get a write lock. To minimize
// this time, there are EnterUpgradeableReadLock/ExitUpgradeableReadLock
// methods. We get a read lock and read the data; if we find that we have to
// change the underlying collection, we just upgrade our lock using the
// EnterWriteLock method, then perform a write operation quickly, and release a
// write lock using ExitWriteLock.
//
// In our case, we get a random number; we then get a read lock and check if
// this number exists in the dictionary keys collection. If not, we upgrade our
// lock to a write lock and then add this new key to a dictionary. It is a good
// practice to use try/finally blocks to make sure we always release locks after
// acquiring them.
//
// All our threads have been created as background threads and after waiting for
// 30 seconds, the main thread as well as all the background threads complete.
//

namespace Chapter2.Recipe8
{
    class Program
    {
        static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
        static Dictionary<int, int> _items = new Dictionary<int, int>();

        static void Main(string[] args)
        {
            new Thread(Read) { IsBackground = true }.Start();
            new Thread(Read) { IsBackground = true }.Start();
            new Thread(Read) { IsBackground = true }.Start();

            new Thread(() => Write("Thread 1")) { IsBackground = true }.Start();
            new Thread(() => Write("Thread 2")) { IsBackground = true }.Start();

            Thread.Sleep(TimeSpan.FromSeconds(30));
        }

        static void Read()
        {
            Console.WriteLine("Reading contents of a dictionary");
            while (true)
            {
                try
                {
                    _rw.EnterReadLock();
                    foreach (var key in _items.Keys)
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(0.1));
                    }
                }
                finally
                {
                    _rw.ExitReadLock();
                }
            }
        }

        static void Write(string threadName)
        {
            while (true)
            {
                try
                {
                    int newKey = new Random().Next(250);
                    _rw.EnterUpgradeableReadLock();
                    if (!_items.ContainsKey(newKey))
                    {
                        try
                        {
                            _rw.EnterWriteLock();
                            _items[newKey] = 1;
                            Console.WriteLine("New key {0} is added to adictionary by a {1}",
                                              newKey, threadName);
                        }
                        finally
                        {
                            _rw.ExitWriteLock();
                        }
                    }
                    Thread.Sleep(TimeSpan.FromSeconds(0.1));
                }
                finally
                {
                    _rw.ExitUpgradeableReadLock();
                }
            }
        }
    }
}