Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#

Tasks, BackgroundWorkers, and Threads – Simple Comparisons for Concurrency

Rate me:
Please Sign up or sign in to vote.
3.00/5 (1 vote)
22 Jan 2023CPOL5 min read 10.5K   5   12
How Tasks, Threads, and BackgroundWorkers operate at high level
In this article, we will explore how Tasks, Threads, and BackgroundWorkers operate at a high level.

(This article is intended to be a spiritual successor to this previous entry, and now includes Tasks!)

Even if you’re new to C#, you’ve probably come across at least one of Tasks, Threads, or BackgroundWorkers. With a bit of additional time, it’s likely you’ve seen all three in your journey. They’re all ways to run concurrent code in C# and each has its own set of pros and cons. In this article, we will explore how each one operates at a high level. It’s worth noting that in most modern .NET applications and libraries, you’ll see things converging to Tasks.

The Approach

I’ve gone ahead and created a test application that you can find here. Because this is in source control, it’s possible/likely that it will diverge from what we see in this article, so I just wanted to offer that as a disclaimer for you as the reader.

The application allows us to select different examples to run. I’ll start by pasting that code below so you can see how things work.

C#
using System.Globalization;

internal sealed class Program
{
    private static readonly IReadOnlyDictionary<int, IExample> _examples =
        new Dictionary<int, IExample>()
        {
            [1] = new NonBackgroundThreadExample(),
            [2] = new BackgroundThreadExample(),
            [3] = new BackgroundWorkerExample(),
            [4] = new SimultaneousExample(),
        };

    private static void Main(string[] args)
    {
        Console.WriteLine("Enter the number for one of the following examples to run:");
        foreach (var entry in _examples)
        {
            Console.WriteLine("----");
            var restoreColor = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Cyan;
            Console.WriteLine($"Choice: {entry.Key}");
            Console.ForegroundColor = ConsoleColor.Magenta;
            Console.WriteLine($"Name: {entry.Value.Name}");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine($"Description: {entry.Value.Description}");
            Console.ForegroundColor = restoreColor;
        }

        Console.WriteLine("----");

        IExample example;
        while (true)
        {
            var input = Console.ReadLine();
            if (string.IsNullOrWhiteSpace(input))
            {
                Console.WriteLine("Would you like to exit? Y/N");
                input = Console.ReadLine();
                if ("y".Equals(input, StringComparison.OrdinalIgnoreCase))
                {
                    return;
                }

                Console.WriteLine("Please make another selection.");
                continue;
            }

            if (!int.TryParse(input, NumberStyles.Integer, 
                 CultureInfo.InvariantCulture, out var exampleId) ||
                !_examples.TryGetValue(exampleId, out example))
            {
                Console.WriteLine("Invalid input. Please make another selection.");
                continue;
            }

            break;
        }

        Console.WriteLine($"Starting example '{example.Name}'...");
        Console.WriteLine("-- Before entering example method");
        example.ExecuteExample();
        Console.WriteLine("-- After leaving example method");
    }
}

Threads

Threads are the most basic form of concurrent execution in C#. They are created and managed by the operating system, and can be used to run code in parallel with the main thread of execution. The concept of a thread is one of the most basic building blocks when we talk about concurrency in general for programming. However, it’s also a name of a class that we can directly use in C# for running concurrent code.

Threads allow you to pass in a method to execute. They also can be marked as background or not, where a background thread will be killed off when the application attempts to exit. Conversely, a non-background thread will try to keep the application alive until the thread exits.

Here is an example of creating and starting a new thread:

C#
Thread newThread = new Thread(new ThreadStart(MyMethod));
newThread.Start();

One major advantage of using Threads is that they have a low overhead, as they are managed directly by the operating system. However, they can be more difficult to work with than other concurrent options, as they do not have built-in support for cancellation, progress reporting, or exception handling. In C#, we’ve had access to the Thread object for a long time so it makes sense that other constructs have been built on top of this for us adding additional quality of life enhancements.

Let’s check out the first Thread example:

C#
public void ExecuteExample()
{
    void DoWork(string label)
    {
        while (true)
        {
            Task.Delay(1000).Wait();
            Console.WriteLine($"Waiting in '{label}'...");
        }
    };

    var thread = new Thread(new ThreadStart(() => DoWork("thread")));
    thread.Start();

    Console.WriteLine("Press enter to exit!");
    Console.ReadLine();
}

In the context of our sample application, we would be able to see the method printing to the console while the Thread is running. However, when the user presses enter, the example method would exit and then the program would also try to exit. Because this Thread is not marked as background, it will actually prevent the application from terminating naturally! Try it out and see.

We can directly compare this with the second example, which has one difference: The Thread is marked as background. When you try this example out, you’ll notice that the running thread does not prevent the application from exiting!

Background Workers

BackgroundWorker is a higher-level concurrent execution option in C#. It is a component included in the System.ComponentModel namespace, and generally you see this used in GUI applications. For example, classic WinForms applications would take advantage of these.

Let’s look at our example for BackgroundWorker:

C#
    public void ExecuteExample()
    {
        void DoWork(string label)
        {
            while (true)
            {
                Task.Delay(1000).Wait();
                Console.WriteLine($"Waiting in '{label}'...");
            }
        };

        var backgroundWorker = new BackgroundWorker();
        // NOTE: RunWorkerCompleted may not have a chance to run 
        // before the application exits
        backgroundWorker.RunWorkerCompleted += 
        (s, e) => Console.WriteLine("Background worker finished.");
        backgroundWorker.DoWork += (s, e) => DoWork("background worker");
        backgroundWorker.RunWorkerAsync();

        Console.WriteLine("Press enter to exit!");
        Console.ReadLine();
    }

One major advantage of using a BackgroundWorker is that it has built-in support for cancellation, progress reporting, and exception handling. It is setup slightly different in that event handlers are registered onto the BackgroundWorker. You can additionally have a completion handler and others registered to the object. Like a Thread marked as background, the BackgroundWorker will not block the application from exiting.

Tasks

Tasks are a modern concurrent execution option introduced in C# 4.0 as part of the Task Parallel Library (TPL). They are similar to Threads, but are managed by the .NET runtime rather than the operating system. Here is an example illustrating just how simple it is to run a Task in the background:

Task.Run(() => SomeMethod());

The major advantage of using Tasks is that they have built-in support for cancellation, progress reporting, and exception handling, similar to BackgroundWorker. Additionally, they are easier to work with than Threads, as they are managed by the .NET runtime. Tasks also support async/await pattern which allow you to write asynchronous code that looks and behaves like synchronous code.

In our final example, we can see a Task being setup and run along side a Thread and a BackgroundWorker. There’s no async/await demonstrated here as I wanted to go for as direct of a comparison as I could.

C#
    public void ExecuteExample()
    {
        var cancellationTokenSource = new CancellationTokenSource();
        void DoWork(string label)
        {
            while (!cancellationTokenSource.IsCancellationRequested)
            {
                Task.Delay(1000).Wait();
                Console.WriteLine($"Waiting in '{label}'...");
            }
        };

        var thread = new Thread(new ThreadStart(() => DoWork("thread")));
        thread.Start();

        var backgroundWorker = new BackgroundWorker();
        // NOTE: RunWorkerCompleted may not have a chance to run 
        // before the application exits
        backgroundWorker.RunWorkerCompleted += 
        (s, e) => Console.WriteLine("Background worker finished.");
        backgroundWorker.DoWork += (s, e) => DoWork("background worker");
        backgroundWorker.RunWorkerAsync();

        var task = Task.Run(() => DoWork("task"));

        Console.WriteLine("Press enter to exit!");
        Console.ReadLine();
        cancellationTokenSource.Cancel();
    }

In the above code, we can see the usage of a CancellationToken. This allows us to interrupt the execution of the loops when they attempt their next iteration. However, a Task is much like a BackgroundWorker and a Thread marked as background in that starting up a Task will not prevent an application from exiting.

Summary – Tasks FTW?

Options for providing concurrency in C# have evolved over the years. Tasks appear to be the go-to choice now for implementing concurrent patterns. However, if you need maximum performance and control, Threads may be an alternate choice. BackgroundWorkers could be a useful choice if you are looking for running some work and having simple support for progress reporting. Of these options, if you need a modern and easy-to-use option with support for async/await pattern, Tasks may be the best choice.

All of these are tools at your disposal, and hopefully you feel more informed as to what each of them does.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Microsoft
United States United States
I'm a software engineering professional with a decade of hands-on experience creating software and managing engineering teams. I graduated from the University of Waterloo in Honours Computer Engineering in 2012.

I started blogging at http://www.devleader.ca in order to share my experiences about leadership (especially in a startup environment) and development experience. Since then, I have been trying to create content on various platforms to be able to share information about programming and engineering leadership.

My Social:
YouTube: https://youtube.com/@DevLeader
TikTok: https://www.tiktok.com/@devleader
Blog: http://www.devleader.ca/
GitHub: https://github.com/ncosentino/
Twitch: https://www.twitch.tv/ncosentino
Twitter: https://twitter.com/DevLeaderCa
Facebook: https://www.facebook.com/DevLeaderCa
Instagram:
https://www.instagram.com/dev.leader
LinkedIn: https://www.linkedin.com/in/nickcosentino

Comments and Discussions

 
QuestionThere is no best practice Pin
kiquenet.com5-Feb-23 10:26
professionalkiquenet.com5-Feb-23 10:26 
AnswerRe: There is no best practice Pin
Dev Leader5-Feb-23 13:54
Dev Leader5-Feb-23 13:54 
BugPoor examples Pin
Сергій Ярошко23-Jan-23 8:16
professionalСергій Ярошко23-Jan-23 8:16 
GeneralRe: Poor examples Pin
Dev Leader25-Jan-23 4:54
Dev Leader25-Jan-23 4:54 
Thanks for your feedback. This is simply to serve as an introduction for folks that have seen these terms come up, but have not seen or read about situations in which they can be use.

Of course, there are plenty of additional examples that could be written here. Perhaps a good follow up article would be actually creating examples of progress reporting across all three mechanisms.

Thank you for the criticism.
Generalaccessing shared memory Pin
Member 1019550523-Jan-23 6:54
professionalMember 1019550523-Jan-23 6:54 
GeneralRe: accessing shared memory Pin
Dev Leader25-Jan-23 4:57
Dev Leader25-Jan-23 4:57 
GeneralRe: accessing shared memory Pin
hulinning225-Jan-23 13:14
hulinning225-Jan-23 13:14 
Questionnice start Pin
Sacha Barber23-Jan-23 1:46
Sacha Barber23-Jan-23 1:46 
AnswerRe: nice start Pin
Jul Titov24-Jan-23 2:59
Jul Titov24-Jan-23 2:59 
GeneralRe: nice start Pin
Dev Leader25-Jan-23 5:00
Dev Leader25-Jan-23 5:00 
Questionforeground thread Pin
Mark Pelf 23-Jan-23 0:54
mvaMark Pelf 23-Jan-23 0:54 
AnswerRe: foreground thread Pin
Dev Leader25-Jan-23 5:01
Dev Leader25-Jan-23 5:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.