Reviewing Threads in C#

Review of multi-threaded C# applications.

The oldest way to create a Windows thread uses the Thread object.

public static class Program {

Thread thread = new Thread(MethodCalledInThread);
thread.Start();
thread.Join();

}

public static void MethodCalledInThread() {

Thread.Sleep(1000);

}

The Start method fires off the thread and the Join method waits until the thread is done.  The Sleep method just waits for the number of milliseconds in the parameter.

By the way, don't ever do it this way.  Take a look at async / await which is usually the way to go.

However, this is a review of using multi-threading in c#, so we march onward.

Foreground versus Background Threads

A process always continues to run as long as there is at least one foreground thread running in the process.  When there are not foreground threads running, then all background threads are forcibly terminated.

Threads created using the Thread object above are foreground threads by default.  You can set a thread as background using the IsBackground property.

thread.IsBackground = true;

The CLR Thread Pool

With the introduction of .NET into the world of windows came the CLR Thread Pool.  This is basically a cache of reusable threads which are stored in a queue to re-use.  When the queue is exhausted, then a new thread is created and added to the queue for use.  This saves on the expense of creating and destroying threads.

You get one of the threads from the pool like this...

ThreadPool.QueueUserWorkItem(MethodCalledInThread);

The cancellation token can be used to interrupt a thread.

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
ThreadPool.QueueUSerWorkItem(p => MethodCalledInThreadWithCancel(cancellationTokenSource.Token));
//Do something
cancellationTokenSource.Cancel();


public static void MethodCalledInThreadWithCancel(CancellationToken token) {
    for (int i = 0; i < 10000 ; i++) {
        if (token,IsCancellationRequested) {
            break;
        }
        Thread.Sleep(100);
    }
}

System.Threading.Tasks

Next came the Task object which was an improvement to the CLR Thread Pool.

new Task(MethodCalledInThread).Start();

You can also use the same cancellation token...

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Task task = new Task(() => MethodCalledInThreadWithCancel(cancellationTokenSource.Token), cancellationTokenSource.Token);
//Do something
cancellationTokenSource.Cancel();

The task object adds all kinds of new functionality.  For example, you can chain tasks serially with...

Task task2 = task.ContinueWith(AnotherMethodCalledInThread);

There is also a way to create parent / child relationships between tasks.  You can chain tasks together and then use very specific rules to determine code execution.  For example, if you fire off a bunch of threads, and then wait for one of the threads, or all of them to complete, before continuing with code execution.

Threading.Tasks.Parallel

If you have a bunch of work to be done, you can spread out the work across multiple cores like this...

Parallel.ForEach(tasks, tasks => RunTask(task));

Or if you want to run multiple tasks at the same time...

Parallel.Invoke(
    () => Task1(),
    () => Task2(),
    () => Task3()
)

Semaphore Slim

What do you do if you have a piece of code that only works with a limited number of threads at a time?  Create a SemaphoreSlim object, which has a constructors taking 2 arguments.  The first argument is the initial count, and the second argument is the max count.  So, if you want a peice of code that only accepts one thread at a time, then instantiate the semaphoreslim like this...

private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

Then, use a SemaphoreSlim like this...

//Some code runs here.  
//Put up the gate here to limit to only one thread at a time
semaphore.Wait();
try
{
    //Code in here will only be run by a single thread at a time.
} finally
{
    semaphore.Release();
}

It is really important to put that semaphore.Release in a finally clauses.  Otherwise, if you hit an error in the gate, no other thread will ever be able to get in and you will be sitting around wondering why your code is not running.

Now, having written all of that, I am going to say again what I said at the beginning.  Most of the time, the async / await construct is the faster and better option than all of stuff I just reviewed.

Using a Semaphore with Async and Await

Here I demonstrate using a semaphore with the async and await keywords.

public static SemaphoreSlim semaphore = new SemaphoreSlim(1,1);


protected async Task SomeMethod()
{
      //Some code runs here.  
      //Put up the gate here to limit to only one execution context
      await semaphore.WaitAsync();
      try
          {
              //The thread will pass completely through this code, before allowing entry by another execution context.
           }
      finally
      {
           semaphore.Release(1);
      }
}

The difference between semaphore.wait versus semaphore.waitasync is huge.  Semaphore.wait is a blocking call.  The thread will just go to sleep and do nothing while wasting lots of memory.  On the other hand, Semaphore.waitasync saves the current execution context in a state machine state and starts running the next waiting execution context in the queue.  Later, when the semapmore is released, the execution context will be restored to the thread, and execution will continue.

However, async / await do not leverage parallelization on multiple cores.  For that, see
Threading.Tasks.Parallel above.

Comments

Popular Posts