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();
}
}
}
}
}