Synchronous & Asynchronous and MultiThread Programming -8 — TPL & Task class & Async Await & ConfigureAwait

Alperen Öz
14 min readJust now

--

TPL & Task class

Photo by Bethany Legg on Unsplash

TPL, or Task Parallel Library, is a built-in library in both .NET Framework and .NET Core. It is used to facilitate asynchronous and parallel programming more efficiently and easily. TPL helps developers write multithreaded and parallel-processing applications, thereby improving performance and reducing execution times.

Key Features:

  1. Task-Based Programming:
    TPL allows you to execute operations using tasks. Tasks simply represent operations executed on a thread. The Task and Task<TResult> classes are the core building blocks of TPL.
  2. Parallel Loops:
    Using methods like Parallel.For and Parallel.ForEach, you can run loops in parallel. This allows the same operation to be executed concurrently on different data segments.
  3. Asynchronous Programming:
    When combined with async/await, TPL enables the asynchronous execution of long-running operations. This prevents the user interface from freezing and allows the application to respond faster.
  4. Thread Pool Utilization:
    TPL efficiently manages threads by leveraging the ThreadPool in the .NET Framework. It automatically optimizes workloads using the Task Scheduler. Compared to manual thread management, tasks provide a simpler and more effective solution.
  5. Error Handling:
    TPL makes error handling in asynchronous operations easier. Errors can be caught in an organized way through tasks. Using try-catch blocks ensures cleaner error management.

Task Class

The Task class is a widely used class in C# to represent asynchronous operations. It essentially acts as a new thread to perform specific operations, allowing other tasks to continue without blocking the main thread.

However, the Task class does not always create a new thread. It often utilizes threads from the ThreadPool, meaning there is no guarantee that a Task object will always run on a newly created thread.

While both the Task and Thread classes serve similar purposes, they represent different approaches to thread management. The choice between them depends on specific usage scenarios and requirements. Threads offer a lower-level, hardware-near structure, while Tasks, using TPL, manage parallel and asynchronous workflows more efficiently.

Differences Between Task and Thread

Task

  • Used in asynchronous programming.
  • Works seamlessly with modern C# using the async and await keywords.
  • Provides automatic management through .NET by efficiently managing threads with the ThreadPool.
  • Offers better control over asynchronous operations.
  • Uses resources more efficiently due to its automatic management.

Thread

  • Used in scenarios requiring low-level thread management.
  • Allows direct creation and control of threads.
  • Consumes more system resources and provides direct access to the processor’s thread mechanism.
  • Ideal for lower-level parallel operations or specific threading tasks.
  • Since threads are managed manually, resource management can be costly.

In summary, the Thread class provides more manual and low-level thread management compared to the Task class. However, this does not mean the Thread class is obsolete. In certain scenarios, manual and lower-level control over threads can be more appropriate.

The Thread class offers a more manual and lower-level approach to thread management compared to the Task class. However, this does not mean that the Thread class is obsolete or unnecessary. In certain scenarios, manual and low-level control over threads may be the better solution to meet specific requirements.

The Task and Thread classes are designed for different usage scenarios and needs.

The Task class is ideal for asynchronous and short-lived operations, efficiently utilizing resources through the ThreadPool.

The Thread class, on the other hand, is more suitable for low-level control and long-running operations.

Each approach has its own advantages, and it is important to choose the right tool based on the requirements of the scenario at hand.

Task Class and ThreadPool

The Task class represents and runs asynchronous operations. It leverages the ThreadPool to execute these operations in the background.

When methods like Task.Run or Task.Factory.StartNew are used, a Task object is created and executed on a ThreadPool thread in the background.

By working with the TPL (Task Parallel Library) provided by .NET, the Task class simplifies parallel programming and thread management, making it more flexible.

The integration of Tasks with the ThreadPool ensures more efficient resource utilization and enhances system performance.

Task task = Task.Run(() =>
{
// operations here...
// These operations are executed in the background by a thread from the ThreadPool
});

How to Use the Task Class?

To start an asynchronous process, we need a Task instance. There are three different ways to create a Task instance:

1. new Task()

This is the traditional way to create an instance of the Task class. However, the created Task will not start immediately. It will only represent an operation, but the task will not run yet. Therefore, the Start method must be called to execute the Task.

Task task = new Task(() => 
{
Console.WriteLine("Task is running.");
});
task.Start(); // Manually starts the task

In this method, the task is created in an unstarted state, and the operation will only execute once the Start method is explicitly invoked.

2. Task.Run()

With this method, a Task object that has already started and is running on a ThreadPool thread is created. It is one of the most common and simplest ways to use the Task class.

Task task = Task.Run(() => 
{
Console.WriteLine("Task is running on a ThreadPool thread.");
});

3. Task.Factory.StartNew()

  • Creating and running a task with Task.Run offers a shorter and easier way to use tasks, while Task.Factory.StartNew provides more customization options and flexibility. However, there are some differences between the two methods.
  • Both Task.Run and Task.Factory.StartNew can start tasks that contain child tasks. To ensure that child tasks are dependent on the parent task, the TaskCreationOptions.AttachedToParent option should be used. Otherwise, child tasks will run independently.
  • Tasks created with Task.Factory.StartNew use threads from the ThreadPool by default. However, a new thread can be started for the task using the TaskCreationOptions.LongRunning option. These operations are optimized and managed by the TaskScheduler.
  • In general, Task.Run is preferred for simple scenarios, while Task.Factory.StartNew is used when more complex configurations are required.
Task task = Task.Factory.StartNew(() =>
{
// operations...
}, new(), TaskCreationOptions.None, TaskScheduler.Default);

In the example, we can see the flexibility provided by starting a task through TaskFactory, along with its parameters.

Task Class Methods

  • Start
    Starts and runs a Task instance.
Task task = new Task(() =>
{
// Transactions here...
});
task.Start();
  • Wait
    Waits for the completion of a Task.
Task task = new Task(() =>
{
// Transactions here...
});
task.Start();
task.Wait();
  • ContinueWith
    Used to perform a specific action after a Task completes.
Task task = Task.Run(() =>
{
// Transactions here...
});

task.ContinueWith(previousTask =>
{
Console.WriteLine("Task is completed.");
});
  • WaitAll
    Waits for the completion of all given Tasks.
Task.WaitAll(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
);
  • WaitAny
    Waits for the completion of any one of the given Tasks.
Task.WaitAny(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
);
  • WhenAny
    Returns a Task that completes when any of the given Tasks completes.
Task.WhenAny(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
).ContinueWith(completedTask =>
{
Console.WriteLine("One of the tasks completed.");
});
  • WhenAll
    Returns a Task that completes when all of the given Tasks complete.
Task.WhenAll(
Task.Run(() => Console.WriteLine("Task 1")),
Task.Run(() => Console.WriteLine("Task 2")),
Task.Run(() => Console.WriteLine("Task 3"))
).ContinueWith(allTasks =>
{
Console.WriteLine("All tasks completed.");
});

In methods like WhenAny and WhenAll that return a Task, the waiting operation is performed on a separate Task object rather than on the main thread. This means the waiting behavior is asynchronous. To synchronize the waiting operation, the await keyword must be used.

  • Delay
    Puts the thread on hold for a specified amount of time.
Task.Run(async () =>
{
await Task.Delay(15000);
// operations...
});

Similar to the previous examples, without the await keyword, even though the delay request is 15000 ms, the operation will continue without waiting since the delay occurs asynchronously on a different Task.

  • FromCanceled
    Returns a canceled Task.
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Task task = Task.Run(() =>
{
while (!cancellationTokenSource.IsCancellationRequested)
{
// Görevi iptal edene kadar işlemler burada gerçekleştirilir...
}
}, cancellationTokenSource.Token);

cancellationTokenSource.Cancel();
Task cancelledTask = Task.FromCanceled(cancellationTokenSource.Token);
if (cancelledTask.IsCanceled)
{
Console.WriteLine("Task cancelled.");
}
  • FromException
    Returns a Task that represents a faulted state with an exception.
Task faultedTask = Task.FromException(new Exception("An error occurred."));
  • FromResult
    Returns a Task object representing a specified value.
Task<int> resultTask = Task.FromResult(35);
  • Task.Result
    Synchronously waits for the asynchronous operation to complete and returns the result.
    However, there is a risk of deadlock when used on the UI thread or in an ASP.NET context since Result blocks the thread until the operation completes.
  • Task.GetAwaiter().GetResult()
    This usage returns the same result but reduces the risk of deadlock since it avoids blocking with async/await mechanism.
Task<int> task = Task.Run(() => 42);
var result = task.Result;
var result2 = task.GetAwaiter().GetResult();
  • Yield
    Temporarily pauses a Task and yields the processor to other threads.
await Task.Yield();

Task Class Properties

  • CompletedTask
    Returns a completed Task object. This is useful when an immediately completed Task is needed.
Task completedTask = Task.CompletedTask;
if (completedTask.IsCompleted)
{
Console.WriteLine("The task is already completed.");
}
  • CurrentId
    Returns the Id of the currently running Task. This helps in identifying the order or state of the running Tasks.
Task task = Task.Run(() =>
{
int? taskId = Task.CurrentId;
Console.WriteLine($"Current Task Id: {taskId}");
});
task.Wait();
  • Factory
    Property used to create new Task objects.
Task task = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task is running.");
});
  • IsCompleted
    Evaluates whether the Task has completed.
Task task = Task.Run(() => { });
task.Wait();
bool isCompleted = task.IsCompleted; // True
  • IsCanceled
    Evaluates whether the Task has been canceled.
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Run(() =>
{
while (!cts.Token.IsCancellationRequested)
{
// transactions
}
}, cts.Token);

cts.Cancel();
bool isCanceled = task.IsCanceled; // True
  • IsCompletedSuccessfully
    Evaluates whether the Task has completed successfully.
Task task = Task.Run(() => { });
task.Wait();
bool isCompletedSuccessfully = task.IsCompletedSuccessfully; // True
  • Id
    Returns the Id of the Task.
Task task = Task.Run(() => { });
int taskId = task.Id;
  • IsFaulted
    Indicates whether the Task resulted in an error.
Task task = Task.Run(() => throw new Exception("Error"));
try
{
task.Wait();
}
catch { }

bool isFaulted = task.IsFaulted; // True
  • Status
    Provides the current status of the Task.
Task task = Task.Run(() => { });
task.Wait();
TaskStatus status = task.Status; // RanToCompletion
  • AsyncState
    Retrieves the state data associated with the Task.
Task task = Task.Factory.StartNew((state) =>
{
Console.WriteLine(state);
}, "test");

var state = task.AsyncState; // "test"

Critical Information About the Task Class

The tasks running under the Task class are generally built on .NET’s task parallelism concept. However, this does not always mean that a new thread is created.

The Task class provides a high-level mechanism through the .NET Task Parallel Library (TPL), where thread management is handled automatically.

Tasks created with Task.Factory.StartNew can either pull a thread from the ThreadPool or create a new thread. This management is optimized by the Task Scheduler.

The Task Scheduler can run a task on one of the existing threads in the ThreadPool or create a new thread when a task is started. However, there is no guarantee as to which thread a specific Task will run on.

Thus, the Task class offers a modern and flexible approach that allows more efficient use of the ThreadPool. However, in certain situations, it may be more appropriate to use the Thread class directly.

For instance, if there is a need to manage a specific thread or achieve lower-level control, the Thread class can be used. Nevertheless, this approach is less commonly preferred today compared to TPL and the Task class in asynchronous programming processes.

Async & Await

Async

In C#, the async keyword is used to indicate that a method, lambda expression, or anonymous method will perform an asynchronous operation. This keyword makes asynchronous programming simpler, more readable, and more manageable.

Basic Usage of the async Keyword

  • The async keyword indicates that a method will run asynchronously.
  • Methods marked with async usually return Task or Task<T>.
  • The async keyword allows the usage of the await expression within the method. The await expression is used to wait for the completion of an asynchronous operation.

Signing an async Method

To mark a method as async, you need to add the async keyword to its signature. Here is a simple example:

public async Task MyAsyncMethod()
{
// Asynchronous operations are performed here
await Task.Delay(1000); // Wait for 1 second
}

In this example, the MyAsyncMethod is marked as async and contains an await expression.

Task and Task<T> with the async Keyword

Async methods typically return either Task or Task<T>. This allows you to check whether the asynchronous operation has completed.

  • Methods that return Task do not return a value.
  • Methods that return Task<T> return a value.

Example: Async Method Returning Task

public async Task PerformOperationAsync()
{
await Task.Delay(1000); // Wait for 1 second
// Other asynchronous operations
}

Example: Async Method Returning Task<T>

public async Task<int> CalculateValueAsync()
{
await Task.Delay(1000); // Wait for 1 second
return 42; // Operation result
}

Using async with Void

Using void as the return type of async methods is rare. Void-returning async methods are mainly used for event handlers.

Async void methods can pose challenges for error management and result tracking.

Why Should We Avoid Async Void Return Types?

  • Unclear Code Flow:
    Async void methods blur the boundaries between synchronous and asynchronous code, making it harder to manage control and flow.
  • Unmanageable by the Caller:
    Errors occurring in async void methods are not propagated back to the calling code, making it difficult to detect and manage issues.
  • No Return Value:
    Since async void methods do not return any value, the caller cannot check if the method has completed, which can cause issues when the completion of an operation needs to be awaited.
  • Error Management Challenges:
    For example, errors in async Task methods can be caught with try-catch blocks, but errors in async void methods cannot be easily handled.

Await

The await keyword is used to wait for the completion of an asynchronous operation.

  • await can only be used inside methods marked with async.
  • It does not start an asynchronous operation but waits for the result of an existing asynchronous operation.

Basic Usage of the await Keyword

  • await is used on methods or operations that return Task or Task<T>.
  • While waiting for the completion of an asynchronous operation, await returns control to the calling method.

Example: Simple Await Usage

public async Task PerformOperationAsync()
{
await Task.Delay(1000); // Wait for 1 second
// Other asynchronous operations
}

In this example, the Task.Delay method waits for one second, and the await expression waits for the operation to complete.

Behavior of the Await Expression

  • While waiting for the completion of an asynchronous operation, await returns control to the calling method.
  • Once the operation is complete, the code following the await expression resumes execution.
  • await ensures that the code continues without interruption from other operations, such as UI updates.

IMPORTANT!!

You may wonder, “How is it asynchronous if there’s waiting involved?” The answer lies in asynchronous waiting.
When the thread executing the method encounters an await keyword, it does not wait for the method to finish. Instead, it returns to the ThreadPool and can be reassigned to another task if one arrives. This allows the thread to remain available for other operations during the asynchronous operation. Once the awaited operation completes, the code resumes either on a new thread or on the original thread that initiated the asynchronous operation.

Example: Demonstrating the Use of Await

public async Task<string> ReadFileAsync(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
// Start reading the file asynchronously and wait for the operation to complete.
string content = await reader.ReadToEndAsync();
// When the reading operation completes, this code continues.
return content;
}
}

In this example:

  • The reader.ReadToEndAsync() method is called to read the file content asynchronously.
  • The await expression waits for the completion of the file reading while returning control to the calling method.
  • Once the operation completes, the code following the await expression resumes, and the file content is returned.

Details of Await Usage

Seamless Flow of Asynchronous Operations:

  • await ensures that the application does not pause during asynchronous operations.
  • For example, in a UI application, await allows the operation to complete without blocking the UI thread.

Error Management:

  • Asynchronous operations with await can be wrapped in try-catch blocks to manage errors effectively.

Example: Error Management with Await

public async Task<string> FetchDataAsync(string url)
{
try
{
using (HttpClient client = new HttpClient())
{
// Start an asynchronous web request and wait for its completion.
string data = await client.GetStringAsync(url);
// Once the request completes, this code resumes execution.
return data;
}
}
catch (Exception ex)
{
// Error management occurs here.
Console.WriteLine($"Error: {ex.Message}");
return null;
}
}

In this example:

  • The client.GetStringAsync(url) method initiates an asynchronous web request.
  • The await expression waits for the web request to complete, returning control to the calling method in the meantime.
  • Once the request completes, the code after await continues execution. If an error occurs, the try-catch block handles it.

Understanding Await and Asynchronous Flow

  • await makes asynchronous operations appear synchronized, improving code readability and manageability.
  • It allows the code to continue without interruptions while waiting for the completion of an asynchronous operation.

Summary

The await keyword in C# is a powerful tool for waiting on the completion of asynchronous operations. It ensures that the application continues smoothly by returning control to the calling method during the wait. Using await improves code readability and manageability, and error management can be easily handled with try-catch blocks. Once the asynchronous operation completes, the code following the await expression resumes execution.

General Rules and Considerations for Async and Await

  • The async keyword must appear before the return type in the method signature.
  • Methods marked with async should return Task, Task<T>, or void.
  • The await keyword can only be used within a method marked with async.
  • await must precede operations that return Task or Task<T>.
  • Constructors, destructors, and properties cannot be marked with async since Task or Task<T> cannot be used as return types in constructors.
  • It is common practice to append “…Async” to the names of methods that return Task or Task<T>, such as GetDataAsync.
  • Avoid void-returning async methods whenever possible; use Task instead.
  • Resources used in async methods should be released properly (e.g., IAsyncDisposable).

ConfigureAwait

The ConfigureAwait method used with the async and await keywords determines the context in which the code continues after the completion of a synchronous operation.
In other words, the ConfigureAwait method is used to control the relationship between the thread that calls the asynchronous method and the thread that runs it.

.ConfigureAwait(false);

  • Ensures that the continuing code runs independently of the original context.
  • This is especially useful for performance optimization and deadlock prevention.

.ConfigureAwait(true);

  • Ensures that the continuing code runs in the same context from which it was called.

Using ConfigureAwait(false) reduces the cost of context switching, improving performance. This is often preferred in UI applications to keep the UI thread free and allow other threads to operate efficiently.

In ASP.NET Core applications, running the code after an await in the same context can lead to deadlocks. To avoid such situations, ConfigureAwait(false) is used.

Example of Context Switching with ConfigureAwait

In this example, we will see how ConfigureAwait(false) and ConfigureAwait(true) affect the thread context. We will simulate running on a UI thread and observe how different ConfigureAwait settings change the thread IDs.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main(string[] args)
{
Console.WriteLine($"Main Thread ID: {Thread.CurrentThread.ManagedThreadId}");
await ExampleWithConfigureAwaitFalse();
await ExampleWithConfigureAwaitTrue();
}
static async Task ExampleWithConfigureAwaitFalse()
{
Console.WriteLine($"\nExampleWithConfigureAwaitFalse - Initial Thread ID: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000).ConfigureAwait(false);
Console.WriteLine($"ExampleWithConfigureAwaitFalse - After ConfigureAwait(false) Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
static async Task ExampleWithConfigureAwaitTrue()
{
Console.WriteLine($"\nExampleWithConfigureAwaitTrue - Initial Thread ID: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000).ConfigureAwait(true);
Console.WriteLine($"ExampleWithConfigureAwaitTrue - After ConfigureAwait(true) Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
}

Output:

Main Thread ID: 1
ExampleWithConfigureAwaitFalse - Initial Thread ID: 1
ExampleWithConfigureAwaitFalse - After ConfigureAwait(false) Thread ID: 9
ExampleWithConfigureAwaitTrue - Initial Thread ID: 9
ExampleWithConfigureAwaitTrue - After ConfigureAwait(true) Thread ID:

Explanation:

  • In the Main method, the initial thread ID is printed.
  • When ExampleWithConfigureAwaitFalse is called, the asynchronous Task.Delay(1000) operation runs with ConfigureAwait(false), allowing the subsequent code to continue on a different thread.
  • When ExampleWithConfigureAwaitTrue is called, the Task.Delay(1000) operation runs with ConfigureAwait(true), ensuring that the subsequent code continues in the same context (on the same thread).

From the output:

  • After using ConfigureAwait(false), a different thread ID is observed for the code running after the await.
  • When ConfigureAwait(true) is used, the same thread ID is maintained after the await.

Source: Gençay Yıldız — Asenkron & Multithread Programlama

--

--