Threads and Tasks: await a minute now!

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

threads and tasks

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:

  1. Checks the status of the task, if the task is already completed, it proceeds with the next statement.
  2. 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

Leave a Comment