In this section, we’ll delve into parallel and asynchronous programming in C#, particularly addressing the frequent mistake of overusing threads rather than leveraging tasks.
It’s important to understand that a Task is an abstraction built on top of the threading model. In many cases, a task does not need to be linked to a specific thread. Tasks provide a wealth of features, such as timeout handling, cancellation support, and continuation management. They can be awaited, nested, and configured with options like designating a task as long-running or associating it with a synchronization context. These features are not available with basic threads, which merely represent a flow of execution on a CPU core.
The following images illustrate the relationship between tasks and threads.
I recommend that, unless you have specific advanced needs that require direct control over threads, you should generally prefer using tasks over threads whenever possible. It’s also important to note that while tasks typically abstract the underlying thread, they can utilize different threads as needed.
Practical Use Case for Tasks
Let’s consider a practical example: a method called PrintTime()
that continuously prints the current time in a loop.
To use this as a task, we invoke it with Task.Run()
, which results in the time being printed at one-second intervals. It’s important to note that the thread spawned by the task operates as a background thread, allowing the main process to continue running without being blocked. To prevent the application from terminating too early, you may need to include a blocking mechanism, such as Console.ReadKey()
.
Exploring Alternative Methods to Create Tasks
Now, let’s look at some alternative methods for creating tasks:
In the first example, we directly generate a result from an integer. This method is effective when your task doesn’t require a thread, as it allows for immediate result return. Importantly, in this scenario, the task abstraction remains independent of any specific thread.
The second example introduces the concept of a ValueTask
, a task object allocated on the stack. This approach is beneficial because it avoids unnecessary memory allocation, particularly in resource-intensive loops.
Lastly, we explore a third method that allows explicit control over when a task concludes. This involves creating a TaskCompletionSource
, which lets you specify the moment the task should complete using t.SetResult()
. These techniques provide greater flexibility in task creation.
These are various ways to create tasks, and we will delve into more details on C# asynchronous programming in the upcoming sections.
Source: Nguyen Chi Hieu – Technical Manager