Synchronous & Asynchronous and MultiThread Programming -8 — TPL & Task class & Async Await & ConfigureAwait
TPL & Task class
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:
- Task-Based Programming:
TPL allows you to execute operations using tasks. Tasks simply represent operations executed on a thread. TheTask
andTask<TResult>
classes are the core building blocks of TPL. - Parallel Loops:
Using methods likeParallel.For
andParallel.ForEach
, you can run loops in parallel. This allows the same operation to be executed concurrently on different data segments. - Asynchronous Programming:
When combined withasync/await
, TPL enables the asynchronous execution of long-running operations. This prevents the user interface from freezing and allows the application to respond faster. - 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. - Error Handling:
TPL makes error handling in asynchronous operations easier. Errors can be caught in an organized way through tasks. Usingtry-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
andawait
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, whileTask.Factory.StartNew
provides more customization options and flexibility. However, there are some differences between the two methods. - Both
Task.Run
andTask.Factory.StartNew
can start tasks that contain child tasks. To ensure that child tasks are dependent on the parent task, theTaskCreationOptions.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 theTaskCreationOptions.LongRunning
option. These operations are optimized and managed by the TaskScheduler. - In general,
Task.Run
is preferred for simple scenarios, whileTask.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 sinceResult
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 returnTask
orTask<T>
. - The
async
keyword allows the usage of theawait
expression within the method. Theawait
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 withasync
.- 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 returnTask
orTask<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 anawait
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 returnTask
,Task<T>
, orvoid
. - The
await
keyword can only be used within a method marked withasync
. await
must precede operations that returnTask
orTask<T>
.- Constructors, destructors, and properties cannot be marked with
async
sinceTask
orTask<T>
cannot be used as return types in constructors.
- It is common practice to append “…Async” to the names of methods that return
Task
orTask<T>
, such asGetDataAsync
. - 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 asynchronousTask.Delay(1000)
operation runs withConfigureAwait(false)
, allowing the subsequent code to continue on a different thread. - When
ExampleWithConfigureAwaitTrue
is called, theTask.Delay(1000)
operation runs withConfigureAwait(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 theawait
. - When
ConfigureAwait(true)
is used, the same thread ID is maintained after theawait
.