In C#, a Thread
is a low-level construct for creating and managing threads, while a Task
is a higher-level construct for creating and managing asynchronous operations.
Let’s Look at Basics First!
A Thread
represents a single execution thread, and provides methods for starting, stopping, and interacting with the thread. You can use Thread
to create new threads, set their priority, and manage their state. When you create a new thread, it begins executing the method you specify.
A Task
represents an asynchronous operation that can be started and awaited. A Task
can be used to execute a piece of code in the background, and the Task
object can be used to track the status and results of the operation. When you create a new task, it begins executing the method you specify on a new thread, but you can also use the Task.Run
method to run the task on a thread pool thread instead of creating a new thread.
In general, you would use Thread
when you need fine-grained control over the execution of a piece of code, such as when you need to manage the thread’s priority or set the thread’s state. On the other hand, you would use Task
when you need to perform an asynchronous operation, such as when you need to perform an I/O-bound operation, or when you need to perform a piece of computation in the background.
It is also worth mentioning that Task
is part of the TPL (Task Parallel Library) and provides more functionality than Thread, such as support for continuation, exception handling, and cancellation.
Thread: Deep Dive
Creating and managing threads at a low level involves working with the operating system’s threading API, which typically includes functions for creating and destroying threads, setting thread priorities, and synchronizing access to shared resources.
Here are some of the low-level details involved in creating and managing threads:
- Thread creation: The operating system provides a function for creating a new thread. This function takes a pointer to a function (often called the “thread function”) that will be executed by the new thread, as well as an optional argument that will be passed to the thread function. The operating system also allows to set the thread’s priority and stack size.
- Thread scheduling: The operating system schedules threads to run on a CPU by using a scheduler. The scheduler assigns a time slice to each thread and switches between threads based on their priority and other factors.
- Thread synchronization: When multiple threads are executing concurrently, they may need to access shared resources, such as memory or file handles. To ensure that these resources are accessed safely and consistently, the operating system provides synchronization primitives such as semaphores, mutexes, and critical sections.
- Thread termination: The operating system provides a function for terminating a thread. A thread can also terminate itself by returning from its thread function, but it’s also possible to use the abort method which is not recommended.
- Thread communication: The operating system provides a way for threads to communicate with each other. For example, Windows provides events, semaphores and message queues. On the other hand, Linux provides pipes and message queues.
- Thread exception handling: Each thread has its own exception handler and the operating system provides a way to handle exceptions within a thread.
Task and Threadpool: What is the relation?
The ThreadPool
is a pool of worker threads that can be used to perform tasks concurrently. It is a part of the .NET Framework and it is designed to minimize the overhead of creating and managing threads.
The Task
class is built on top of the ThreadPool
. When you create a new Task
and call the Task.Start()
or Task.Run()
method, the Task
schedules the work to be done by the ThreadPool
. ThreadPool
then assigns an available thread from the pool to execute the task.
The Task.Run
method is a shorthand for creating a new Task
and starting it. It creates a new Task
and schedules it to be executed on the ThreadPool
. It also returns a Task
object that can be used to track the status and results of the operation.
Task
class provides a higher-level abstraction for working with threads than the Thread
class, and it is a part of the TPL (Task Parallel Library) which is a set of libraries and APIs that allow you to write concurrent and parallel code more easily. The TPL abstracts away the low-level details of creating and managing threads, and provides a simpler and more powerful model for working with concurrent and parallel code.
Under the hood, the Task
class schedules the work to be done by the ThreadPool
, and the ThreadPool
assigns an available thread to execute the task. The Task
class also provides additional functionality such as support for continuation, exception handling, and cancellation.

Let’s look into an example; shall we?
Here’s an example of how you could use a Task
to perform an I/O-bound operation asynchronously:
using System;
using System.Threading.Tasks;
using System.IO;
class Program
{
static void Main()
{
// Start a new task to perform the I/O-bound operation
Task<int> task = ReadFileAsync("file.txt");
// Perform other operations while the I/O-bound operation is in progress
Console.WriteLine("Doing other work...");
// Wait for the task to complete
int fileLength = task.Result;
// Print the length of the file
Console.WriteLine("File length: " + fileLength);
}
static async Task<int> ReadFileAsync(string fileName)
{
using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
{
byte[] buffer = new byte[stream.Length];
await stream.ReadAsync(buffer, 0, buffer.Length);
return buffer.Length;
}
}
}
But what does this code do?
In this example, the ReadFileAsync
method starts an I/O-bound operation asynchronously by reading the contents of a file into a byte array using the FileStream
class. The ReadFileAsync
method is marked as async
, which allows the method to use the await
keyword. The await
keyword is used to indicate that the call to ReadAsync
should be made asynchronously.
In the Main
method, we start a new task by calling the ReadFileAsync
method and passing it the file name. The ReadFileAsync
method returns a Task<int>
object, representing the ongoing asynchronous operation. The Main
method can then continue to execute while the ReadFileAsync
method reads the contents of the file in the background.
The Main
method then prints “Doing other work…”, to indicate that it is performing other operations while the I/O-bound operation is in progress.
Finally, the Main
method uses the Result
property of the Task<int>
object to wait for the task to complete and get the result of the asynchronous operation, which is the length of the file.
It’s worth noting that in this example, we used the FileStream
class, but the same concept can be applied to other classes like WebClient
, HttpClient
that perform I/O bound operations.
Task.start vs Task.run
Task.Start()
is used to start a task that has been created using the new
keyword, and it schedules the task to be executed by the ThreadPool
. It’s important to note that calling Start()
on a task that is already running or has already completed will result in an exception being thrown.
On the other hand, Task.Run()
is a shorthand for creating a new task and starting it. It creates a new Task
and schedules it to be executed on the ThreadPool
, it also returns a Task
object that can be used to track the status and results of the operation. This method is a convenient way to create and start a new task in one line of code.
Await a minute!
The await
keyword is used to indicate that the method should be executed asynchronously. When the await
keyword is used, the method returns a Task
or Task<T>
object, which represents the ongoing asynchronous operation. The await
keyword does not create a new thread, instead, it allows the calling method (in this case, ReadFileAsync
) to yield execution back to the calling method (in this case, Main
) while the asynchronous operation (in this case, ReadAsync
) is in progress.
When the await
keyword is used with a method that returns a Task
or Task<T>
, the compiler generates code that:
- Checks the status of the task, if the task is already completed, it proceeds with the next statement.
- If the task is not completed, the method registers a continuation to be executed when the task completes and returns control to the calling method.
In the ReadFileAsync
example, the ReadAsync
method is an asynchronous method, it returns a Task
that represents the ongoing I/O operation. The await
keyword is used to tell the compiler to schedule the continuation of the ReadFileAsync
method after the ReadAsync
task completes. By doing so, the ReadFileAsync
method releases the thread to perform other tasks while the I/O operation is in progress, so the other parts of the code can continue executing without waiting for the I/O operation to complete.
For more post like this; you can also follow this profile – https://dev.to/asifbuetcse