NashTech Insights

Common Misconceptions in C# and .NET – Overusing Threads Instead of Tasks

Hieu Nguyen
Hieu Nguyen
Table of Contents

In this section, we will explore the world of parallel and asynchronous programming in C#, with a particular focus on the common mistake of overusing threads instead of employing tasks.

When comparing tasks and threads, it’s essential to understand that a Task is an abstraction that operates atop the threading model. However, there exist numerous scenarios where a task need not be intricately tied to a specific thread. Tasks offer a rich set of features, including timeout handling, cancellation mechanisms, and continuation management. They can be awaited and nested, and various options can be configured, such as designating a task as long running or associating it with a synchronization context. These capabilities are absent in basic threads, which simply represent an execution flow on a CPU core.

The following images will show scenarios which could explain the relationship between Task and Thread.

I recommend that, unless you have advanced requirements necessitating exclusive control over threads, you should almost always prefer using tasks over threads whenever feasible.  Additionally, it’s important to note that while tasks often abstract away the underlying thread, they can encapsulate different threads at a later stage.

Let me present a practical use-case for Tasks. Consider a method called “PrintTime()” that continuously prints the current time within a while loop.

To utilize this as a task, we invoke it via “Task.Run()”, causing it to print the time at one-second intervals. It’s noteworthy that the thread created by the task is a background thread, meaning it won’t impede the overall process. To prevent the process terminating prematurely, an additional blocking mechanism like “Console.ReadKey()” is needed.

Next, let’s explore alternative methods to create tasks:

The first example, we directly generate a result from an integer. This approach is suitable when your task doesn’t involve a thread, as it allows you to return results directly. Notably, in this case, the task abstraction remains detached from any specific thread.

The second example introduces the concept of a “ValueTask”, which is a task object allocated on the Stack memory. This is advantageous as it avoids unnecessary memory allocation, especially when tasks are invoked within resource-intensive loops.

Lastly, we explore a third method that provides explicit control over when a task concludes. This involves creating a “TaskCompletionSource” and specifying the moment when the task should complete by using “t.SetResult()”. These techniques offer flexibility in task creation.

These are the different ways to create a task. We will get in more details in the coming topics relate to C# asynchronous programming.

Hieu Nguyen

Hieu Nguyen

Hieu is an Engineering Manager at NashTech with 20+ years of experience in software development, he is specializing in the Microsoft stack have in-depth knowledge and experience with Microsoft technologies such as .NET framework, C#, ASP.NET, SQL Server, Azure, and Visual Studio. He is in a leadership role that involves overseeing and guiding the technical teams involved in software engineering projects.

Suggested Article

%d bloggers like this: