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 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:
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.