Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game / XBox

Non Deadlocking Read/Write Locker and Thread-Safe Collections

2.75/5 (7 votes)
6 Oct 2010MIT5 min read 58.6K   266  
A ReaderWriterLock that cannot deadlock, and thread-safe example collections

Introduction

This is a Read-Write Locker which cannot be deadlocked. It supports intuitive upgrade locks and allows a high level of recursion. This means you no longer have to keep track of how many times and what order threads are locking a resource, greatly increasing manageability.

ReaderWriterLock Wrapper Class is a CodeProject article about wrapping the .NET ReaderWriterLock class into using disposable objects that can automatically allow upgrade locks. But it has been my experience that upgrading locks using ReaderWriterLock is not thread safe due to a bug within the class.

This class uses a locker designed from scratch to not only allow all the functionality found in ReaderWriterLock, but also perform better and handle situations where deadlocks would otherwise occur.

Image 1

Understanding the Deadlock Issue

Part A

lock(A)  // 1. Thread 1 has entered, no other threads can enter lock(A)
         // in Part A or Part B.
{
   sleep(20);  // Sleeps 20 milliseconds to demonstrate race condition failing.
   lock(B)      // 3.
   {

   }
}

Part B

lock(B)  // 2. Thread 2 has entered, no other threads can enter lock(B)
         // in Part A or Part B.
{
   sleep(20);  // Sleeps 20 milliseconds to demonstrate race condition failing.
   lock(A)      // 4.
   {

   }
} 

The above is pseudo-code which demonstrates how hard locking occurs. Each lock will only allow one thread to enter it. If two threads enter, the first one that enters goes through, while any other threads are paused at that locks position.

Thread 1 enters lock(A) in Part A. Then it sleeps for a short bit. Thread 2 enters lock(B) in Part B. Then it sleeps a bit too. Then at #3 in Part A, Thread 1 gets held by trying to lock on lock(B), which is already held by Thread 2. Then at #4 in Part B, Thread 2 gets held by lock(A), causing a hard lock.

The Halting Issue in Regards to Deadlocks

A common misconception is that in order to stop deadlocks, you must solve the n-complete halting issue. This is, however, wrong.

"Detecting the possibility of a deadlock before it occurs is much more difficult and is, in fact, generally undecidable, because the halting problem can be rephrased as a deadlock scenario" - http://en.wikipedia.org/wiki/Deadlock.

This is misleading because it gives the impression that within the locking mechanism, we have no control over the condition in which halting occurs. The Halting problem states, "The proof shows there is no total computable function that decides whether an arbitrary program i halts on arbitrary input x."

Distributed deadlock prevention within the article shows that the halting issue and locking are mutually exclusive, because all conditions within it are known.

Therefore, this article and distributed deadlock prevention are in fact one possible general solution to preventing deadlock.

Quote

"If I shook a magic 8 ball with digital text that I could change after seeing the initial answer, I believe that "without-a-doubt, all signs point to yes" (given I had the room to write that)." -- TamusJRoyce.

Solving the Deadlock Issue

My solution is to allow this hard lock to almost occur. But I keep track of all threads coming in. So when the number of threads that have entered read and write locks equals the number of threads being locked (checked right before the lock occurs), I let that thread "run its course."

By run its course, I mean I track it after assigning it as a super thread. If it enters other locks while being the super thread, it blocks all other threads until this otherwise hard-locked thread is finished, and unlocks. So this hard-lock prevention scheme is recursive.

This idea had to be applied twice. Once for the read-write locker itself (Local DeadLock Prevention), and the second globally between all read-write lockers (Distributed DeadLock Prevention). Below only shows Local DeadLock Prevention for simplicity.

C#
// Class to replace regular locks which one instance alone cannot be deadlocked.
public class RegularLock
{
  private class LockObject : IDisposable
  {
    RegularLock _lock;

    public LockObject(RegularLock __lock)
    {
      _lock = __lock;
    } // End Constructor

    public IDisposable GetLock()
    {
      // Begins locking
      _lock.LockFunction();
      return this;
    } // End GetLock() Method

    public void Dispose()
    {
      _lock.UnlockFunction();
    } // End Dispose() Method
  } // End LockObject class

  //          ManagedThreadId, Number of times each thread recursively locked.
  Dictionary <int,            int> ThreadsEntered  = new Dictionary<int,int>();

  //          ManagedThreadId, Number of times each thread recursively locked.
  Dictionary <int,             int> ThreadsLocked = new Dictionary<int,int>();

  int SuperThreadId    = -1;
  int SuperThreadCount = 0;

  LockObject lockObj = new LockObject(this);

  public IDisposable GetLock()
  {
    // Magic here is that lockObject isn't created each time it is used!
    return lockObject.GetLock();
  } // End GetLock() Method

  bool LockIsNeeded(int currentThreadId)
  {
    lock(ThreadsEntered)
    {
      lock(ThreadsLocked)
      {
        // Guarantees that a hard lock will occur.  Then lets the super thread continue.
        return SuperThreadId != currentThreadId;
      } // End lock
    } // End lock
  } // End LockIsNeeded() Function

  // This could be either read locking, write locking, or normal
  //   locking, depending on how LockIsNeeded() is setup.
  void LockFunction()
  {
    Dim CurrentThreadId As Integer = Thread.CurrentThread.ManagedThreadId;
    Dim CountValue      As Integer

    // This is part of the super thread's "Running its course."
    if (SuperThreadId == CurrentThreadId)
    {
      SuperThreadCount += 1;
    }

    lock(ThreadsEntered)
    {
      if (ThreadsEntered.TryGetValue(CurrentThreadId, CountValue))
      {
        ThreadsEntered(CurrentThreadId) = CountValue + 1;
      } else {
        ThreadsEntered.Add(CurrentThreadId, 0);
      } // End if
    } // End lock

    if (LockIsNeeded(CurrentThreadId))
    {
      lock(ThreadsLocked)
      {
        if (ThreadsLocked.TryGetValue(CurrentThreadId, CountValue))
        {
          ThreadsLocked(CurrentThreadId) = CountValue + 1;
        } else {
          ThreadsLocked.Add(CurrentThreadId, 0);
        } // End If
      } // End lock

      while (LockIsNeeded(CurrentThreadId))
      {
        lock(ThreadsEntered)
        {
          lock(ThreadsLocked)
          {
            if (SuperThreadId == -1 && ThreadsEntered.Count == ThreadsLocked.Count)
            {
              SuperThreadId = CurrentThreadId;
            } // End if
          } // End lock
        } // End lock

        if (SuperThreadId == CurrentThreadId)
        {
          break;
        } // End if

        Thread.Wait(6000);
      } // End while

      lock(ThreadsLocked)
      {
        if (ThreadsLocked.TryGetValue(CurrentThreadId, CountValue))
        {
          if (CountValue == 0)
          {
            ThreadsLocked.Remove(CurrentThreadId);
          } else {
            ThreadsLocked(CurrentThreadId) = CountValue - 1;
          } // End if
        } // End if
      } // End lock
    } // End [LockIsNeeded()] if
  } // End LockFunction() Function

  void UnlockFunction()
  {
    Dim CurrentThreadId As Integer = Thread.CurrentThread.ManagedThreadId;
    Dim CountValue        As Integer

    lock(ThreadsEntered)
    {
      if (ThreadsEntered.TryGetValue(CurrentThreadId, CountValue))
      {
        if (CountValue == 0)
        {
          ThreadsEntered.Remove(CurrentThreadId);
        } else {
          ThreadsEntered(CurrentThreadId) = CountValue - 1;
        } // End if
      } // End if
    } // End lock

    lock(ThreadsEntered)
    {
      lock(ThreadsLocked)
      {
        // Only after the super thread has ran its course is
        // another thread allowed to take its place.
        if (SuperThreadCount == 0 && SuperThreadId == CurrentThreadId)
        {
          SuperThreadId = -1;
        }

        // This is part of the super thread's "Running its course"
        if (SuperThreadId == CurrentThreadId)
        {
          SuperThreadCount -= 1;
        } // End if
      } // End lock
    } // End lock
  } // End UnlockFunction() Function
} // End class

Using the Code

The code below shows detailed methods in using this read-write locker. There are more examples to be found here.

VB.NET
Imports System
Imports System.Threading
Imports System.Collections.Generic

Public Module MainProgram
  Dim Lock   As New ReadWriteLocker(True)
  Dim List   As New List(Of Integer)(New Integer {0,5,0,4,0,3,0,2,0,1})
  Dim IsBusy As Integer = 0

  Public Sub main()
    InitializeThreads()
  End Sub

  Public Sub InitalizeThreads()
    For Index As Integer = 0 To 25
      Interlocked.Increment(IsBusy)
      ThreadPool.QueueUserWorkItem(FunctionToRunInMultipleThreads, Index)
    Next

    While IsBusy > 0
      Thread.Sleep(500)
    End While
  End Sub

  Public Sub FunctionToRunInMultipleThreads(threadContext As Object)
    Dim threadIndex As Integer = CInt(threadContext)
    Console.WriteLine("thread {0} started...", threadIndex);

    Using ReadLock As IDisposable = Lock.GetReadLock()
      ' Iterate through list
      For Index As Integer = 0 To List.Count - 1
        List(Index) += Index
      Next

      ' Upgrade read lock to write lock.  Yes.  It is that simple.
      Using WriteLock As IDisposable = Lock.GetWriteLock()
        For Index As Integer = 0 To List.Count - 1
          List.Add(List(Index))
        Next
      End Using

      ' Iterate through list
      For Index As Integer = 0 To List.Count - 1
        Console.WriteLine("List value: {0}, on thread {1}.", List(Index), threadIndex)
      Next
    End Using

    Interlocked.Decrement(IsBusy)
  End Sub
End Class

Replacing ReaderWriterLock

The included NHLReaderWriterLock is designed to be a drop-in replacement for .NET's ReaderWriterLock. NHLReaderWriterLock is designed for correctness by having thread-safe upgrade locks. It also performs better when deadlock prevention is turned off (comparable when it is turned on).

Other than replacing ReaderWriterLock with NHLReaderWriterLock and including this library in your projects references, no other changes need to be made.

Replacing ReaderWriterLockSlim

There is a NHLReaderWriterLockSlim included as well. But my goal with it is not to replace ReaderWriterLockSlim, but rather detect potential deadlocks so you are able to fix them. And then switch back to using ReaderWriterLockSlim.

If at some point in the future ReadWriteLocker gets setup using spin locks, helping it match ReaderWriterLockSlim's performance, then it may be a viable option towards replacing it.

Stance on Stopping all Deadlocks

Under certain circumstances, this it will in fact deadlock. But those circumstances are external to this locker. To include those, I would have to solve the n-complete halting problem to guarantee they don't cause deadlocks!

I have in no way done that. That is far above my head. But just like if you don't allow recursive locking with ReaderWriterLockSlim, you won't get deadlocks; if you do the same with this locker, you won't get deadlocks either.

I, however, have extended those circumstances to include recursion and upgrading and downgrading locks. So long as other locking mechanisms run mutually exclusive to this lock (they can be used within this lock non-recursively, but not around this lock), and each lock that gets initiated gets unlocked (and hence, no halting problem involved!), deadlocks will not happen.

Collection which Caches Changes During Enumeration

Below is the code showing a collection with the ability to choose whether it is thread-safe or not. Now why would we want to turn off thread-safety while using this locker? Because it contains events which tell us when iterating over the list completes.

So this thread-locker even with thread-safety disabled, will tell our collection when it should cache modifications, and when it should write those modifications directly to the collection. This means that you can perform a for-each on this ThreadSafeList and insert/add/change/remove/clear, it will not modify the list until after the for-each finishes.

And the same goes for multi-threaded instances where the collection is being read from. Even if writing is allowed, you may not want data changed while it is being read. This class is a good example how to setup any resource in which the resource must remain unchanged until reading is finished.

VB.NET
' This example is for ThreadSafeList(Of T) Collection.
Imports NoHardLockReadWriteLocker
Imports ThreadSafeClasses

Public Module MainProgram
  Public Sub main()
    InitializeThreads()
  End Sub

  // List which has thread safety turned on, and initialized to an array of integers.
  Dim List As New ThreadSafeList(Of Integer)(True, New Integer {0,5,0,4,0,3,0,2,0,1})
  Dim IsBusy As Integer = 0

  Public Sub InitalizeThreads()
    For Index As Integer = 0 To 10
      Interlocked.Increment(IsBusy)
      ThreadPool.QueueUserWorkItem(FunctionToRunInMultipleThreads, Index)
    Next

    While IsBusy > 0
      Thread.Sleep(500)
    End While
  End Sub

  Public Sub FunctionToRunInMultipleThreads(threadContext As Object)
    Dim threadIndex As Integer = CInt(threadContext)
    Console.WriteLine("thread {0} started...", threadIndex);

    ' Thread-safety does not need to be turned on for 
    ' just the modifying while enumerating.
    For Each Item As Integer In List
      ' This normally results in a enumeration error!
      List.Add(Item)

      Console.WriteLine("Value {0} found...note that items added in _
			for-each won't appear yet.", Item);
    Next

    For Each Item As Integer In List
      Console.WriteLine("Value {0} found...note that items added in _
			previous for-each appear!", Item);
    Next

    ' This is not thread-safe!  This needs a read lock wrapped around it.
    While (List.Count > 17)
      ' This can throw an error if List.Count is grabbed, an item is removed
      ' via another thread.  Then RemoveAt is called.  That position would be gone.
      List.RemoveAt(List.Count)
    End While

    ' This is thread-safe.  Notice the read lock wrapped around it.
    '
    ' But this will loop forever, because removing an item will be cached
    ' and the count will never decrease.
    Using ReadLock As IDisposable = List.GetReadLock()
      While (List.Count > 14)
        List.RemoveAt(List.Count)
      End While
    End Using

    ' This is correctly thread-safe.
    While (List.Count > 10)
      Using ReadLock As IDisposable = List.GetReadLock()
        List.RemoveAt(List.Count)
      End Using
    End While

    Interlocked.Decrement(IsBusy)
  End Sub
End Module 

Below is an overview of the implementation that makes this thread-safe list work.

VB.NET
Imports System
Imports System.Threading
Imports System.Collections.Generic

Imports NoHardLockReadWriteLocker

Public Class ThreadSafeList(Of T)
 Implements IList(Of T)

  ' Both classes within this region are accessed within thread safe locking.
  ' Therefore, thread safety does not need to be done within them.
  #Region "Classes"
    ' This is the object that caches modifications to this collection.
    Private Class ModificationCache
      Private Enum ModificationType
        Added
        Removed
        Cleared
      End Enum

      ' This is used to tells what modification to make when applying this cache.
      Private Structure Item
        Public Type As ModificationType
        Public Item As T
        Public Index As Integer

        Public Sub New(ByVal __type As ModificationType, _
		ByVal __item As T, ByVal __index As Integer)
          Type = __type
          Item = __item
          Index = __index
        End Sub
      End Structure

      ' List to apply changes, when requested.
      Private _List As List(Of T)
      Private _Items As New List(Of Item)()

      Public Sub New(__list As List(Of T))
        _List = __list
      End Sub

      Public Sub Add(item As T)
        ' Adds a modification to the cache.
        _Items.Add(New Item(ModificationType.Added, item, -1))
      End Sub

      ...

      ' This is different than the other modification caching functions.
      '   It clears all the previously cached modifications and caches that a
      '   clear need to be performed.
      Public Sub Clear()
        _Items.Clear()
        _Items.Add(New Item(ModificationType.Cleared, Nothing, -1))
      End Sub

      ...
    End Class

    ' Wrapper around an iterator which disposes both the Iterator and ReadLock.
    Private Class ThreadSafeIterator
      Implements IEnumerator(Of T)

      Private _Iterator As IEnumerator(Of T)
      Private _ReadLock As IDisposable
      Private _IsDisposed As Boolean = False

      Public Sub New(ByVal __iterator As IEnumerator(Of T), _
		ByVal __readLockObject As IDisposable)
        _Iterator = __iterator
        _ReadLock = __readLockObject
      End Sub

      Public ReadOnly Property Current() As T _
        Implements IEnumerator(Of T).Current

        Get
          Return _Iterator.Current
        End Get
      End Property

      ...

      Public Sub Dispose()
        _Iterator.Dispose()
        _ReadLock.Dispose()
      End Sub
    End Class
  #End Region

  Private WithEvents _Locker As ReadWriteLocker
  Private _List As List(Of T)
  Private _ModificationCache As ModificationCache

  ' Thread-Safety is an option for this class. 
  ' Remember to set it to True if you need it.
  Public Sub New(Optional ByVal __isThreadSafe As Boolean = False)
    Dim List As New List(Of T)()

    _Locker = New ReadWriteLocker(__isThreadSafe)
    _List = List
    _ModificationCache = New ModificationCache(List)
  End Sub

  ...

  Public ReadOnly Property Count() As Integer _
    Implements ICollection(Of T).Count

    Get
      ' The first instance of locking so far.  As you see in the above sample,
      '   this is really meaningless in a multi-threaded environment unless
      '   you wrap your own Read Lock around this and whatever else is using Count.
      Using ReadLock As IDisposable = _Locker.GetReadLock()
        Return _List.Count

        ' Todo:  Don't Read-Lock, and throw an exception if this is called
        '        and it is not Read-Locked, and Thread-Safety is enabled!
      End Using
    End Get
  End Property

  Default Public Property Item(ByVal index As Integer) As T _
    Implements IList(Of T).Item

    Get
      Using ReadLock As IDisposable = _Locker.GetReadLock()
        Return _List.Item(index)
      End Using
    End Get
    Set(ByVal value As T)
      Using WriteLock As IDisposable = _Locker.GetWriteLock()
        If _Locker.IsReading Then
          _ModificationCache.Assign(_List(index), Value)
        Else
          _List(index) = Value
        End If
      End Using
    End Set
  End Property

  Public Function Contains(ByVal item As T) As Boolean _
    Implements ICollection(Of T).Contains

    Using ReadLock As IDisposable = _Locker.GetReadLock()
      Return _List.Contains(item)
    End Using
  End Function

  ...

  Public Function GetEnumerator() As IEnumerator(Of T) _
    Implements IEnumerable(Of T).GetEnumerator

    ' Starts Read-Locking before the ThreadSafeIterator is even made.
    Dim ReadLock As IDisposable = _Locker.GetReadLock()

    ' Returns a thread-safe iterator.
    Return New ThreadSafeIterator(_List.GetEnumerator(), ReadLock)
  End Function

  Public Sub Add(ByVal item As T) _
    Implements ICollection(Of T).Add

    Using WriteLock As IDisposable = _Locker.GetWriteLock()
      If _Locker.IsReading() Then
        ' Caching modifications if they happen while reading is occurring.
        _ModificationCache.Add(item)
      Else
        _List.Add(item)
      End If
    End Using
  End Sub

  ...

  Public Sub Clear() _
    Implements ICollection(Of T).Clear

    Using WriteLock As IDisposable = _Locker.GetWriteLock()
      If _Locker.IsReading() Then
        _ModificationCache.Clear()
      Else
        _List.Clear()
      End If
    End Using
  End Sub

  ' When iteration is complete, an event is fired that indicates
  ' a read lock has been released.
  '   When there is no more read locks, apply the changes that have been cached.
  Protected Overridable Sub OnUnlocked() _
    Handles _Locker.UnlockedForReading

    ' Doesn't raise the _Locker.UnlockedForReading event, 
    ' as write locking is being done.
    Using WriteLock As IDisposable = _Locker.GetWriteLock()
      If Not _Locker.IsReading Then
        _ModificationCache.PerformCachedModifications()
      End If
    End Using
  End Sub
End Class

History

  • Started off as a wrapper to ReaderWriterLock in November of 2009.
  • Then it was modified to use ReaderWriterLockSlim in March, 2010.
  • And lastly it was re-implemented using ReaderWriterLock Alternative, which finally exposes enough functionality I could accurately prevent deadlocks in September, 2010.

License

This article, along with any associated source code and files, is licensed under The MIT License