Threading in C#

Threads are an important concept in asynchronous programming. In this post we will cover creating threads and give a brief overview of some of the low-level synchronisation classes in .NET. Finally we look at the .NET thread pool which allows a series of tasks to be run in parallel.

Creating threads

The Thread class can be found in the System.Threading namespace.

A thread can be created using the following constructor:

public Thread(ThreadStart threadStart);

where the ThreadStart delegate is defined as follows:

public delegate void ThreadStart();

A thread can be started by calling Start(). We can wait for a thread to finish by calling Join().

Here is an example:

class ThreadDemo
{
    public void ThreadFn()
    {
        Console.WriteLine("Thread started...");
    }

    public void Run()
    {
        Thread thread = new Thread(ThreadFn);
        thread.Start();
        thread.Join();
    }
}

To pass a parameter into the thread function, there is an alternative Thread constructor:

public Thread(ParameterizedThreadStart threadStart);

where the ParameterizedThreadStart delegate has the following signature:

public delegate void ParameterizedThreadStart(object obj);

The thread data can then be passed as a parameter in the Thread.Start() method.

Our example then looks like:

class ThreadDemo
{
    public void ThreadFn(object obj)
    {
        Console.WriteLine("Thread started...");
    }

    public void Run()
    {
        Thread thread = new Thread(ThreadFn);
        thread.Start(57);
        thread.Join();
    }
}

For reference, the following diagram lists some of the key methods in Thread:

Thread class

Thread synchronisation

The lock keyword can be used to prevent multiple threads accessing the same resource simultaneously.

An example is shown below:

Object myLock = new Object();
lock(myLock)
{
    // Access the protected resource...
}

The C# System.Threading namespace also has a host of useful synchronisation classes. Some of these are shown in the UML class diagram below:

WaitHandle

A Mutex can be used in the same way as a lock in order to protect a resource. A Mutex can also be shared between processes.

AutoResetEvent and ManualResetEvent both allow events to be signalled between different threads. An AutoResetEvent will automatically reset itself after an awaiting thread has been released.

ThreadPool

Suppose we want to run a series of unrelated tasks in parallel to speed up execution.

We already have the mechanisms in place to achieve this (i.e. mutexes and events). But the actual implementation requires a bit of thought. For example, we have to decide how many threads we want to run concurrently and queue up the remaining work until a thread becomes available.

Luckily, the System.Threading namespace has a built in class called ThreadPool to do this for us.

We can simply call the static function:

bool ThreadPool.QueueUserWorkItem(
    WaitCallback callback, Object object);

where callback is a delegate to specify the function that does the work and the optional object parameter can be used to pass any data to the thread.

When we call QueueUserWorkItem(), the thread pool will immediately start working on our task in the background.

The one remaining challenge is knowing when all our tasks have completed. This is something we still have to manage ourselves.

Here is an example showing one approach:

public class ThreadData
{
    public int JobId { get; }
    public ManualResetEvent Event { get; }

    public ThreadData(int jobId, ManualResetEvent eventHandle)
    {
        JobId = jobId;
        Event = eventHandle;
    }
}

class ThreadDemo
{
    private void ThreadFn(object obj)
    {
        ThreadData threadData = obj as ThreadData;
        Console.WriteLine("Performing work item " + threadData.JobId);
        Thread.Sleep(1000);
        Console.WriteLine("Finished work item " + threadData.JobId);
        threadData.Event.Set();
    }

    public void Run()
    {
        var events = new List<ManualResetEvent>();
        for (int i = 0; i < 10; i++)
        {
            var eventHandle = new ManualResetEvent(false);
            events.Add(eventHandle);
            ThreadPool.QueueUserWorkItem(ThreadFn, new ThreadData(i, eventHandle));
        }

        WaitHandle.WaitAll(events.ToArray());
    }
}

In the example, we create 10 tasks and run them using the built in ThreadPool class. Each task is allocated a ManualResetEvent. When each task is finished, it signals completion by setting the event.

We then use the static WaitAll() method to await the completion of all 10 tasks.

Note that this is just example code to demonstrate the use of ThreadPool and ManualResetEvent. Creating an event for each task might not be the most efficient approach for production code.

An alternative approach might be to countdown the number of remaining tasks by decrementing each time a task is completed. There is even a CountdownEvent class in C# to make life easy!

Note also that WaitHandle objects implement the IDisposable interface so there should also be a policy of disposing of them as soon as they are no longer required.