Task-Based Asynchronous programming in C#.NET is very effective in scaling and improved throughput. Though it uses the thread pool where it queues up the tasks. But with sophisticated algorithms, TPL becomes easy to use. Also with the use of Async Await in C#, it improves the usability of the application. In this article, we will cover Task in C# and using a task with async-await in c#.
This article is in continuation of our previous articles related to multi-threading in C#, if you have not gone through those articles we recommend you to go through them.
Below is the high-level concept that we will be covering in this entire article.
- Task in C# & Creating a new task in C# with Task.Run & TaskFactory Methods.
- How to run multiple task Parallel using Task Array.
- Continuation After Task is Completed & Catching Exceptions.
- CPU, IO Operations & Async Await in C# with Example.
- Await Multiple tasks using When All.
What is a Task in C#?
A Task (System.Threading.Tasks.Task) is an operation that can be run parallel, It may return value also. Task Generics provides a way to specify the type which will be returned from the task after completion. In this section, we will try to see the basics and will cover the advanced topics of upcoming articles.
Creation & Running a Task
Task Run Method
The Creation of the task and starting the task need not be separate, we can use Task.Run()
method to create and start the task as below,
Task task=Task.Run(() => { content = ReadFile("C:/Test", "test.txt"); });
TaskFactory
There is one more way we can create and run the task, which is Task.Factory.StartNew()
, with this we have control over the Task, we can pass state, creation options, etc . with this syntax. Also, we can get the state using Task.AsyncState.
Task task=Task.Factory.StartNew(() => { content = ReadFile("C:/Test", "test.txt"); });
Task Library has also got the generic version where we can specify the type which comes handy where can return the data in a type-safe way. Since we are reading the data from the file , we can store the data in string type. We can use task.result
property to read the results after task completes.
Task Example
Let’s get started with creating and running tasks, this can be done in several ways, Below example, show the simple invocation of Task and by creating Task for running a read file operation. Assume that the read file operation is independent, we can call this ReadFile
method in the lambda expression. When the task is executed, we can continue with other operations inside the Main function. task.wait()
will wait for method to finish.
using System; using System.IO; using System.Threading.Tasks; namespace Learn { class Program { //Reading File ,Independent and Time consuming process public static string ReadFile(string path, string fileName) { Console.WriteLine("Beginning ReadFile Program"); string fullPath = path + "/" + fileName; string content = ""; using (StreamReader rdr = new StreamReader(fullPath)) { content = rdr.ReadToEnd(); } Console.WriteLine("End of ReadFile Program"); return content; } static void Main(string[] args) { Console.WriteLine("Beginning Main Program"); string content = ""; Task task = Task.Run(() => { content = ReadFile("C:/Test", "test.txt"); }); Console.WriteLine("Other Parts of Main Program"); Console.WriteLine("........................."); Console.WriteLine("........................."); task.Wait(); Console.WriteLine("File Contents:"); Console.WriteLine(content); Console.WriteLine("End of Main Program"); } } }
By using factory the task will look like this
Task<string>task=Task<string>.Factory.StartNew(() => { return ReadFile("C:/Test", "test.txt"); }); Console.WriteLine(task.Result);
Task Factory with State
The above factory method has got many overloads where we can pass state, creation options, etc. Let’s pass the state to the Task. Here we have created a new class to hold the directory and filename of the file, the object is passed to the Task.Parallel.StartNew()
method.
Code snippet is below
Task<string> task = Task<string>.Factory.StartNew( (Object obj) => { SampleFile f = obj as SampleFile; return ReadFile(f.directory, f.fileName); }, file);
The Complete example code is below.
using System; using System.IO; using System.Threading.Tasks; namespace Learn { public class SampleFile { public string fileName; public string directory; public SampleFile(string fileName, string directory) { this.fileName = fileName; this.directory = directory; } } class Program { //Reading File ,Independent and Time consuming process public static string ReadFile(string path, string fileName) { Console.WriteLine("Beginning ReadFile Program"); string fullPath = path + "/" + fileName; string content = ""; using (StreamReader rdr = new StreamReader(fullPath)) { content = rdr.ReadToEnd(); } Console.WriteLine("End of ReadFile Program"); return content; } static void Main(string[] args) { Console.WriteLine("Beginning Main Program"); SampleFile file = new SampleFile("test.txt", "C:/Test"); Task<string> task = Task<string>.Factory.StartNew( (Object obj) => { SampleFile f = obj as SampleFile; return ReadFile(f.directory, f.fileName); }, file); Console.WriteLine("Other Parts of Main Program"); Console.WriteLine("........................."); Console.WriteLine("........................."); task.Wait(); Console.WriteLine("File Contents:"); Console.WriteLine(task.Result); Console.WriteLine("End of Main Program"); } } }
Run Multiple Tasks in Parallel using Task Array
With Task Array, We can run multiple tasks in parallel & wait for all the tasks to complete before proceeding further. For example, say we have multiple files in the directory and we need to read the content of the files. To achieve this we can modify the previous example slightly. Task.WaitAll(tasArray)
can be used to wait for more than tasks. It expects an array of tasks as the argument. In this example, we have used List<Task<int>>
. We have added sample files under the same directory, the name of the files are test,test1,test2 etc. We are looping over the list of files and proving the state to Task.Factory.StartNew()
method.
using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace Learn { public class SampleFile { public string fileName; public string directory; public SampleFile(string fileName, string directory) { this.fileName = fileName; this.directory = directory; } } class Program { public static string ReadFile(string path, string fileName) { string fullPath = path + "/" + fileName; string content = ""; using (StreamReader rdr = new StreamReader(fullPath)) { content = rdr.ReadToEnd(); } return content; } static void Main(string[] args) { List<SampleFile> files = new List<SampleFile>(); files.Add(new SampleFile("test.txt", "C:/Test")); files.Add(new SampleFile("test1.txt", "C:/Test")); files.Add(new SampleFile("test2.txt", "C:/Test")); files.Add(new SampleFile("test3.txt", "C:/Test")); List<Task<string>> tasks = new List<Task<string>>(); foreach (SampleFile file in files) { tasks.Add(Task<string>.Factory.StartNew((Object obj) => { SampleFile f = obj as SampleFile; return ReadFile(f.directory, f.fileName); }, file)); } Console.WriteLine("File Contents:"); Task.WaitAll(tasks.ToArray()); foreach (Task<string> task in tasks) { Console.WriteLine($"File Name : {(task.AsyncState as SampleFile).fileName}"); Console.WriteLine(task.Result); Console.WriteLine(); } } } }
Continuation of Task after Completion in C#
In Task-Based Asynchronous programming if we want to execute an operation immediately after the previous tasks completes we can achieve that using the Task Continuation feature in Task & Task<TResult> library. This comes in handy when passing the results of the antecedent tasks to the next operation on completion.
Task.ContinueWith
and Task<TResult>.ContinueWith
is the static method that is available with Task within the Task library. Let’s look at the below example.
using System; using System.IO; using System.Threading.Tasks; namespace Learn { class Learn { public static string ReadFile(string path, string fileName) { string fullPath = path + "/" + fileName; string content = ""; using (StreamReader rdr = new StreamReader(fullPath)) { content = rdr.ReadToEnd(); } return content; } public static void Main(string[] args) { Task<string> task = Task.Factory.StartNew<string>( () => { return ReadFile("c:/Test", "test.txt"); } ); task.ContinueWith((result) => { Console.WriteLine(result.Result); }); task.Wait(); } } }
Here ReadFile is a sample method that reads the content from the file. We need to pass the file path and file name to this method. We need to immediately print the content of the file after the task completed. So we can specify the operation which needs to be performed once the FileRead method executed.
In this example, we are just printing the contents of the file. Since we specified the type as string, Task<String> type is passed to the ConinueWith method.
Chaining ContinueWith in Task
We can also chain the ContinueWith method if we have subsequent operations as below,
task .ContinueWith((result) => { Console.WriteLine(result.Result); }) .ContinueWith((x) => { Console.WriteLine("................."); });
Cancellation of Task in C#
In the previous sections, we have seen how Task-based implementation can be used to improve the performance of an application by creating a task ad waiting for it to complete when it is required.
There may be a scenario where we want to cancel the task in response to a user request. This can be easily achieved using the overloads available in the Task Parallel Library. While using Task.Factory.StartNew
there is an overload that accepts the Cancellation Token as additional arguments. Let’s look at an example of how easy to cancel a task before it completes.
using System; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Learn { class Learn { public static string ReadFile(string path, string fileName, CancellationToken token) { token.ThrowIfCancellationRequested(); string fullPath = path + "/" + fileName; string content = ""; using (StreamReader rdr = new StreamReader(fullPath)) { string line; while ((line = rdr.ReadLine()) != null) { if (token.IsCancellationRequested) { token.ThrowIfCancellationRequested(); } Console.WriteLine(line); } } return content; } public static void Main(string[] args) { CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; Task<string> task = Task.Factory.StartNew<string>( () => { return ReadFile("c:/Test", "test.txt", token); } , token ); tokenSource.Cancel(); try { task.Wait(); } catch (AggregateException ae) { Console.WriteLine("Task Canceled"); } catch (Exception ex) { Console.WriteLine("Task Canceled"); } } } }
From the above example, we can see that we have created a CancellationTokenSource
object to get a token. The token then passed to the task creation method. Inside the ReadFile
method, code is written which will poll if Task Cancellation is requested while it reads each line from the file. If the Task Cancellation is requested then it will throw a Task canceled exception. Once the task successfully canceled the Task status will be changed to Canceled. When we use task.wait()
an AggregateException
will be thrown.
Catching Exceptions from Task in C#
Handling Exceptions in tasks is similar to catching other exceptions. We can wrap the Task.Wait()
, Task.WaitAll()
, await task
, etc in a try-catch block. When a task throws an exception we can catch it from the Catch block. The type of exception thrown is AggregateException
.Task AggregateException
has the InnerExceptions property where we can see all the exceptions which were thrown. We can loop over the exception list to analyze the exceptions. In a similar way, if a child Task throws an exception it returns AggregateException, a parent tasks also its own AggregateException with wrapping child exception.
We can use the flatten option to loop over the exceptions. If the exception thrown is expected we can handle that by AggregateException.Handle method. Where we can return true if the exception is handled properly, else we can throw the exception. When the task is canceled we can throw OperationCanceledException. Let’s take an example to understand how exceptions can be caught.
using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace Learn { class Program { public static string ReadFile(string path, string fileName) { string fullPath = path + "/" + fileName; string content = ""; using (StreamReader rdr = new StreamReader(fullPath)) { content = rdr.ReadToEnd(); } return content; } static void Main(string[] args) { Task<string> task = Task<String>.Factory.StartNew(() => { return ReadFile("c:/test", "testxx.txt"); }); try { task.Wait(); } catch (AggregateException ex) { foreach (Exception item in ex.InnerExceptions) { Console.WriteLine(item.Message); } } } } }
In the above example we have intentionally specified a wrong file name just to catch FileNotFound Exception.We can catch AggregateException and under InnerExceptions we can see the FileNotFoundException.
Async and Await in C#
What is Async & Await in C#?
Async and await are used in C# for waiting for an operation asynchronously which returns the task. In Synchronous programming, if a certain method takes some time to complete the entire application has to wait till it gets completed. If we are considering the improvement of the overall usability of an application such as non-blocking UI etc then async and await is the better choice.
c# async await example example
Task<string> readFileTask = ReadFileAsync("c:/test","test.txt") //....other operations... string output =await readFileTask;
Why do we need Asynchronous Programming using Async & Await?
Async & Await improves the overall usability of the application. Imagine an application that has long-running operations such as reading a large file or accessing the web etc.
When one of such operations runs, the UI becomes unresponsive( Example: WPF & WinForms). The user who is using the application may think like something went wrong & assumes the application might be stopped working. So by using Asynchronous programming we can make the application more usable & responsive. We can leverage Async and Await to make the UI responsive & continue doing other works until the potentially blocking code finishes.
What is CPU vs IO Operations?
Mainly there could be two types of operations that we are performing, one is CPU oriented, and the other is IO related. In CPU-related operations, more CPU time needed meaning, without CPU time we cant do those operations. An example of CPU intensive operation would be Image Processing related code, where it involves lots of calculations. But an IO related task would not be needing CPU time, such as reading a large file, accessing the web, etc.
As we have seen in the previous tutorials we can use Task.Run()
to do operations which are CPU intensive & which will be run in a separate thread. But for the second type of operations which are not CPU intensive, we can leverage the use of Async and Await pattern to improve the efficiency of the code. We don’t need additional threads for just waiting for an operation to complete, we just need a promise that we will continue once the operation is complete. The combination of async & await does the same. The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. We can also effectively write the asynchronous code for CPU intensive tasks using async & await in combination with Task.Run()
Does Async & Await in C# cause to spawn additional threads?
Using async and await won’t create additional threads as it is running on the current synchronization context. It uses the time on the current thread only when it is active.
How to use Async & Await in C#?
Let us take a sample program to see how we can use async & await for writing a non-blocking code.
In the above code let us assume that DoSomeWorkAsync is a method that needs to be run asynchronously, we can use async and await for doing this. Let us see a few requirements for this.
- DoSomeWorkAsync method returns a “Task“
- DoSomeWorkAsync method marked as “async’“
- There is an await statement inside the DoSomeWorkAsync method.
- By convention, the method name ends with “Async” to indicate that this method is available.
- When the task’s result needed we can use “await’” to retrieve the results.
If you see the output of the above method it can be noted that DoSomeWorkAsync
ran asynchronously & await Task.Delay(2000)
used to simulate the operation taking 2 seconds of time to complete. In the main method, the await statement kept as the last statement.
So, the task started to run & control returns to the caller, here it is the Main method & executes other work till a point it can’t further until the awaited operation is complete. When the Task operation which needs to be executed asynchronously has a return type we can useTask<TResult>
for those methods.
Let us see another example that reads a file asynchronously.
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; namespace Learn { class Program { public static async Task<string> ReadFileAsync(string path, string fileName) { Console.WriteLine("Insise the ReadFileAsync method"); string fullPath = path + "/" + fileName; StringBuilder content = new StringBuilder(); using (FileStream streamToRead = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true)) { byte[] buffer = new byte[0x1000]; int numbersToRead; while ((numbersToRead = await streamToRead.ReadAsync(buffer, 0, buffer.Length)) != 0) { string text = Encoding.UTF8.GetString(buffer, 0, numbersToRead); content.Append(text); } } Console.WriteLine("Returning from ReadFileAsync method"); return content.ToString(); } public async static Task Main(string[] args) { Console.WriteLine("Inside Main Program"); //Run the read file operation asynchronously Task<string> readFileTask = ReadFileAsync("c:/test", "test.txt"); Console.WriteLine("Other methods to Run While ReadFileAsync running.........."); //after this point , we can't conitnue without readFileTask completed string output = await readFileTask; Console.WriteLine("output:" + output); Console.WriteLine("End Main Program"); } } }
The method ReadFileAsync is an asynchronous method with return type as Task<string>. The code Task<string> readFileTask = ReadFileAsync("c:/test","test.txt")
starts the ReadFileAsync asynchronously, Once the task started code continues to execution for which the ReadFileAsync result is not needed. Once those code completed string output =await readFileTask;
code waits for the result to be available as we can’t continue further without the output from ReadFileAsync.Once the output available execution resumes from here.
If we don’t have any other code to execute we can combine the code to start the task & awaiting the response,
string output=await ReadFileAsync("c:/test","test.txt");
The above image shows the flow of code execution. The lines are marked with numbers from 1 through 10.And the output of the code is as below. (output contains some automobile brands’ name).
Waiting for Multiple Asynchronous Tasks using Task.WhenAny
//when any one of the tasks completes await Task.WhenAll(tasks) //when all the tasks completes await Task.WhenAny(tasks)
There are many methods available under Task Library which helps us with different requirements. We can use await Task.WhenAll(tasks)
, await Task.WhenAny(tasks)
while working with multiple asynchronous tasks. We can start multiple asynchronous tasks & perform some actions as and when they complete. Let’s extend the previous example to read multiple files asynchronously. We have created a simple class named SampleFile for storing the fileName and directoryName of the file, we have created a list of SampleFile & added a few sample files. We are running a loop and starting new tasks to read the contents from the file asynchronously & storing the contents as and when they complete & finally printing the contents.
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; namespace Learn { //Sample class to hold fileName & directory public class SampleFile { public string fileName; public string directory; public SampleFile(string fileName, string directory) { this.fileName = fileName; this.directory = directory; } } class Learn { public static async Task<string> ReadFileAsync(string path, string fileName) { Console.WriteLine("Inside the ReadFileAsync method,fileName:{0}", fileName); string fullPath = path + "/" + fileName; StringBuilder content = new StringBuilder(); using (FileStream streamToRead = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true)) { byte[] buffer = new byte[0x1000]; int numbersToRead; while ((numbersToRead = await streamToRead.ReadAsync(buffer, 0, buffer.Length)) != 0) { string text = Encoding.UTF8.GetString(buffer, 0, numbersToRead); content.Append(text); } } Console.WriteLine("Returning from ReadFileAsync method,fileName:{0}", fileName); return content.ToString(); } public async static Task Main(string[] args) { Console.WriteLine("Inside Main Program"); List<SampleFile> files = new List<SampleFile>(); files.Add(new SampleFile("test.txt", "C:/Test")); files.Add(new SampleFile("test1.txt", "C:/Test")); files.Add(new SampleFile("test2.txt", "C:/Test")); files.Add(new SampleFile("test3.txt", "C:/Test")); //list of tasks to store the all the tasks List<Task<string>> tasks = new List<Task<string>>(); //Run the read files operation asynchronously foreach (SampleFile file in files) { Task<string> readFileTask = ReadFileAsync(file.directory, file.fileName); tasks.Add(readFileTask); } Console.WriteLine("Other methods to Run While ReadFileAsync running.........."); //after this point , we can't conitnue without readFileTask completed StringBuilder sb = new StringBuilder(); //check if the tasks list empty, if not continue to wait for tasks to complete. while (tasks.Count != 0) { Task<string> completedTask = await Task.WhenAny(tasks); tasks.Remove(completedTask); sb.AppendLine(completedTask.Result); sb.AppendLine(); } Console.WriteLine("output: " + sb.ToString()); Console.WriteLine("End Main Program"); } } }
Below is the output(output may differ slightly on each execution )