Deep C# - Threading,Tasks and Locking
Written by Mike James   
Wednesday, 22 October 2025
Article Index
Deep C# - Threading,Tasks and Locking
Starting A Thread
Ensuring Thread Safety
Exclusion Using Locking
Deadlock
Background Worker
Task Parameters

Background Worker

There is an easy way to implement a thread intended to do some background computation, the BackgroundWorker control. It attempts to make threading seem easy by making it look like event handling. The idea is that the average programmer understands how to write an event handler, but isn’t an expert on threading. The background worker does indeed make things easier, but it still brings with it all of the problems of threading. 

The easiest way to create a background worker is to drag-and-drop it from the Windows Forms toolbox. This automatically generates the code needed for the event handler. If you don’t want to use a control then you can create the background worker in code, it’s only slightly more difficult.

BackgroundWorker BW1 = new BackgroundWorker();

You can also use this construction with WPF.

The key to understanding the way the BackgroundWorker object implements threading for you is to realize that its DoWork event handler is run using a separate thread obtained from the thread pool.

For example, define the event handler as:

BW1.DoWork +=delegate(object s,DoWorkEventArgs We)
{
    	//do something else
};

The event handler has been defined using an anonymous method, but you can define it in the usual way via a named method wrapped in a DoWorkEventHandler delegate if you want to. Nothing happens until we use the command:

BW1.RunWorkerAsync();

This gets a thread from the thread pool and uses it to run the method associated with the DoWork event. You can monitor the new thread using RunWorkerCompleted event and even get a result from the RunWorkerCompletedEventsArg object which is passed back.

There are a few potential well-documented pitfalls, but in the main BackgroundWorker is easy to use except that you can’t access any of the UI components from the DoWork event handler and accessing shared resources is just as much a problem as if you had created the thread explicitly. The documentation simply says the equivalent of “don’t do it”.

There is an argument for keeping BackgroundWorker threads simple, but if you understand threading you can use Invoke in the same way as described to allow UI interaction in the next chapter. For example, you can use the addtext2 method, described in the next chapter using the self invoke approach:

addtext2("thread ending");

at the end of the DoWork event handler and it will display the message in the text box without any cross threading problems.

The BackgroundWorker control does make it possible to get some of the benefits of threading without having to worry about the problems. However, if you know about threading there is little to be gained by using it. It may stop you worrying about the problems, but they are still present.

Task – The .NET Way To Thread

Using threads as described so far is the low-level, yet surprisingly portable, way of implementing asynchronous code. Just about every language and every operating system has some form of threading, but .NET has a higher-level implementation of asynchronous code that is built on top of threads, the Task. A Task is a class that represents an operation that will run on a separate thread. The threads are taken from the collection of already constructed threads waiting to be used – the thread pool. This speeds things up as creating threads is a slow process.

There are two task-related classes, Task and Task<TResult> - the first doesn’t return a result and the second returns a result of the specified type. Already we have something extra to the basic thread as threads need you to provide code to extract results from them.

There are a number of different Task and Task<Tresult> constructors but the simplest are Task(Action) and Task<Tresult>(Func<Tresult>) which create Task objects based on the Action or Func delegates.

The Action delegate is a set of delegates like:

Action()
Action<T1>()
Action<T1,T2>()

and so on and the Func delegate is a set of delegates like:

Func<TResult> 
Func<T1, TResult> 
Func<T1, T2, TResult> 

and so on. They provide delegates suitable for functions with a different number of parameters and a return type. Task has constructors for Action(), Func<TResult), Action<object> and Func<object,Tresult>. That is you can have one or no parameters and a result or no result.

Thus the previous example using threads for functions A and B can be written using Tasks as:

Action AA = new Action(A);
Task T1 = new Task(AA);
Action AB = new Action(B);
Task T2 = new Task(AB);
T1.Start();
T2.Start();
T1.Wait();
T2.Wait();
textBox1.Text = count.ToString();

Notice that use of Wait to halt the current thread until the Task is complete. This isn’t the shortest way to write this code, but it does show the stages of creating the delegate, then the Task and starting it running.

The simplest version is probably:

Task T1 = Task.Run(A);
Task T2 = Task.Run(B);
Task.WaitAll(new Task[] { T1, T2 });
textBox1.Text = count.ToString();

The Run method takes a function of the correct type, creates a Task and runs it. The WaitAll method waits for all of the Task objects in the array to finish. The existence of Wait, WaitAll and WaitAny are good reasons for preferring tasks to threads. There is also When, WhenAll, WhenAny which run a Task when the specified tasks end. Notice that the When methods provide an easy way to chain Task objects so that one starts when one ends.



Last Updated ( Wednesday, 22 October 2025 )