Book Image

Mastering C# Concurrency

Book Image

Mastering C# Concurrency

Overview of this book

Starting with the traditional approach to concurrency, you will learn how to write multithreaded concurrent programs and compose ways that won't require locking. You will explore the concepts of parallelism granularity, and fine-grained and coarse-grained parallel tasks by choosing a concurrent program structure and parallelizing the workload optimally. You will also learn how to use task parallel library, cancellations, timeouts, and how to handle errors. You will know how to choose the appropriate data structure for a specific parallel algorithm to achieve scalability and performance. Further, you'll learn about server scalability, asynchronous I/O, and thread pools, and write responsive traditional Windows and Windows Store applications. By the end of the book, you will be able to diagnose and resolve typical problems that could happen in multithreaded applications.
Table of Contents (17 chapters)
Mastering C# Concurrency
Credits
About the Authors
About the Reviewers
www.PacktPub.com
Preface
Index

Using locks


There are different types of locks in C# and .NET. We will cover these later in the chapter, and also throughout the book. Let us start with the most common way to use a lock in C#, which is a lock statement.

Lock statement

Lock statement in C# uses a single argument, which could be an instance of any class. This instance will represent the lock itself.

Reading other people's codes, you could see that a lock uses the instance of collection or class, which contains shared data. It is not a good practice, because someone else could use this object for locking, and potentially create a deadlock situation. So, it is recommended to use a special private synchronization object, the sole purpose of which is to serve as a concrete lock:

// Bad
lock(myCollection) {
  myCollection.Add(data);
}

// Good
lock(myCollectionLock) {
  myCollection.Add(data);
}`

Note

It is dangerous to use lock(this) and lock(typeof(MyType)). The basic idea why it is bad remains the same: the objects you are locking could be publicly accessible, and thus someone else could acquire a lock on it causing a deadlock. However, using the this keyword makes the situation more implicit; if someone else made the object public, it would be very hard to track that it is being used inside a lock.

Locking the type object is even worse. In the current versions of .NET, the runtime type objects could be shared across application domains (running in the same process). It is possible because those objects are immutable. However, this means that a deadlock could be caused, not only by another thread, but also by ANOTHER APPLICATION, and I bet that you would hardly understand what's going on in such a case.

Following is how we can rewrite the first example with race condition and fix it using C# lock statement. Now the code will be as follows:

const int iterations = 10000;
var counter = 0;
var lockFlag = new object();
ThreadStart proc = () => {
  for (int i = 0; i < iterations; i++)
  {
    lock (lockFlag)
      counter++;
    Thread.SpinWait(100);
    lock (lockFlag)
      counter--;
  }
};
var threads = Enumerable
  .Range(0, 8)
  .Select(n => new Thread(proc))
  .ToArray();
foreach (var thread in threads)
  thread.Start();
foreach (var thread in threads)
  thread.Join();
Console.WriteLine(counter);

Now this code works properly, and the result is always 0.

To understand what is happening when a lock statement is used in the program, let us look at the Intermediate Language code, which is a result of compiling C# program. Consider the following C# code:

static void Main()
{
  var ctr = 0;
  var lockFlag = new object();
  lock (lockFlag)
    ctr++;
}

The preceding block of code will be compiled into the following:

.method private hidebysig static void  Main() cil managed {
  .entrypoint
  // Code size       48 (0x30)
  .maxstack  2
  .locals init ([0] int32 ctr,
                [1] object lockFlag,
                [2] bool '<>s__LockTaken0',
                [3] object CS$2$0000,
                [4] bool CS$4$0001)
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  stloc.0
  IL_0003:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0008:  stloc.1
  IL_0009:  ldc.i4.0
  IL_000a:  stloc.2
  .try
  {
    IL_000b:  ldloc.1
    IL_000c:  dup
    IL_000d:  stloc.3
    IL_000e:  ldloca.s   '<>s__LockTaken0'
    IL_0010:  call       void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
    IL_0015:  nop
    IL_0016:  ldloc.0
    IL_0017:  ldc.i4.1
    IL_0018:  add
    IL_0019:  stloc.0
    IL_001a:  leave.s    IL_002e
  }  // end .try
  finally
  {
    IL_001c:  ldloc.2
    IL_001d:  ldc.i4.0
    IL_001e:  ceq
    IL_0020:  stloc.s    CS$4$0001
    IL_0022:  ldloc.s    CS$4$0001
    IL_0024:  brtrue.s   IL_002d
    IL_0026:  ldloc.3
    IL_0027:  call       void [mscorlib]System.Threading.Monitor::Exit(object)
    IL_002c:  nop
    IL_002d:  endfinally
  }  // end handler
  IL_002e:  nop
  IL_002f:  ret
} // end of method Program::Main

This can be explained with decompilation to C#. It will look like this:

static void Main()
{
  var ctr = 0;
  var lockFlag = new object();
  bool lockTaken = false;

  try
  {
    System.Threading.Monitor.Enter(lockFlag, ref lockTaken);
    ctr++;
  }
  finally
  {
    if (lockTaken)
      System.Threading.Monitor.Exit(lockFlag);
  }
}

It turns out that the lock statement turns into calling the Monitor.Enter and Monitor.Exit methods, wrapped into a try-finally block. The Enter method acquires an exclusive lock and returns a bool value, indicating that a lock was successfully acquired. If something went wrong, for example an exception has been thrown, the bool value would be set to false, and the Exit method would release the acquired lock.

A try-finally block ensures that the acquired lock will be released even if an exception occurs inside the lock statement. If the Enter method indicates that we cannot acquire a lock, then the Exit method will not be executed.

Monitor class

The Monitor class contains other useful methods that help us to write concurrent code. One of such methods is the TryEnter method, which allows the provision of a timeout value to it. If a lock could not be obtained before the timeout is expired, the TryEnter method would return false. This is quite an efficient method to prevent deadlocks, but you have to write significantly more code.

Consider the previous deadlock sample refactored in a way that one of the threads uses Monitor.TryEnter instead of lock:

static void Main()
{
  const int count = 10000;

  var a = new object();
  var b = new object();
  var thread1 = new Thread(
    () => {
      for (int i = 0; i < count; i++)
        lock (a)
      lock (b)
      Thread.SpinWait(100);
  });
  var thread2 = new Thread(() => LockTimeout(a, b, count));
  thread1.Start();
  thread2.Start();
  thread1.Join();
  thread2.Join();
  Console.WriteLine("Done");
}

static void LockTimeout(object a, object b, int count)
{
  bool accquiredB = false;
  bool accquiredA = false;
  const int waitSeconds = 5;
  const int retryCount = 3;
  for (int i = 0; i < count; i++)
  {
    int retries = 0;
    while (retries < retryCount)
    {
      try 
      {
        accquiredB = Monitor.TryEnter(b, TimeSpan.FromSeconds(waitSeconds));
        if (accquiredB) {
          try {
            accquiredA = Monitor.TryEnter(a, TimeSpan.FromSeconds(waitSeconds));
            if (accquiredA) {
              Thread.SpinWait(100);
              break;
            }
            else {
              retries++;
            }
          }
          finally {
            if (accquiredA) {
              Monitor.Exit(a);
            }
          }
        }
        else {
          retries++;
        }
      }
      finally {
        if (accquiredB)
          Monitor.Exit(b);
      }
    }
    if (retries >= retryCount)
      Console.WriteLine("could not obtain locks");
  }
}

In the LockTimeout method, we implemented a retry strategy. For each loop iteration, we try to acquire lock B first, and if we cannot do so in 5 seconds, we try again. If we have successfully acquired lock B, then we in turn try to acquire lock A, and if we wait for it for more than 5 seconds, we try again to acquire both the locks. This guarantees that if someone waits endlessly to acquire a lock on B, then this operation will eventually succeed.

If we do not succeed acquiring lock B, then we try again for a defined number of attempts. Then either we succeed, or we admit that we cannot obtain the needed locks and go to the next iteration.

In addition, the Monitor class can be used to orchestrate multiple threads into a workflow with the Wait, Pulse, and PulseAll methods. When a main thread calls the Wait method, the current lock is released, and the thread is blocked until some other thread calls the Pulse or PulseAll methods. This allows the coordination the different threads execution into some sort of sequence.

A simple example of such workflow is when we have two threads: the main thread and an additional thread that performs some calculation. We would like to pause the main thread until the second thread finishes its work, and then get back to the main thread, and in turn block this additional thread until we have other data to calculate. This can be illustrated by the following code:

var arg = 0;
var result = "";
var counter = 0;
var lockHandle = new object();
var calcThread = new Thread(() => {
  while (true)
  lock (lockHandle) 
  {
    counter++;
    result = arg.ToString();
    Monitor.Pulse(lockHandle);
    Monitor.Wait(lockHandle);
  }
})
{
  IsBackground = true
};
lock (lockHandle) 
{
  calcThread.Start();
  Thread.Sleep(100);
  Console.WriteLine("counter = {0}, result = {1}", counter, result);

  arg = 123;
  Monitor.Pulse(lockHandle);
  Monitor.Wait(lockHandle);
  Console.WriteLine("counter = {0}, result = {1}", counter, result);

  arg = 321;
  Monitor.Pulse(lockHandle);
  Monitor.Wait(lockHandle);
  Console.WriteLine("counter = {0}, result = {1}", counter, result);
}

As a result of running this program, we will get the following output:

counter = 0, result =
counter = 1, result = 123
counter = 2, result = 321

At first, we start a calculation thread. Then we print the initial values for counter and result, and then we call Pulse. This puts the calculation thread into a queue called ready queue. This means that this thread is ready to acquire this lock as soon as it gets released. Then we call the Wait method, which releases the lock and puts the main thread into a waiting queue. The first thread in the ready queue, which is our calculation thread, acquires the lock and starts to work. After completing its calculations, the second thread calls Pulse, which moves a thread at the head of the waiting queue (which is our main thread) into the ready queue. If there are several threads in the waiting queue, only the first one would go into the ready queue. To put all the threads into the ready queue at once, we could use the PulseAll method. So, when the second thread calls Wait, our main thread reacquires the lock, changes the calculation data, and repeats the whole process one more time.

Note

Note that we can use the Wait, Pulse, and PulseAll methods only when the current thread owns a lock. The Wait method could block indefinitely in case no other threads call Pulse or PulseAll, so it can be a reason for a deadlock. To prevent deadlocks, we can specify a timeout value to the Wait method to be able to react in case we cannot reacquire the lock for a certain time period.