Introduction
In this article, you will learn about async
/ await
threading model, what problem it solves and how internally it is implemented. This article assumes that you are already familiar with multi-threaded programming with various different synchronization models. You may learn more about threads and synchronization in my previous article.
This article will take you through a problem statement and a solution using a traditional approach. Then it will solve the same problem using the async
/ await
approach. Towards the end, it will discuss how internally it is implemented.
Problem Statement
You are working at a Windows Form application. In one of the forms, on a click of button, you are calling a method that returns a numeric value which you display on a text box. This method takes an average 10~20 secs for completion. Your client has complained that during this period, the application remains unresponsive. Some of the users have even restarted the application with the assumption that it just hanged. Following is the related code:
public partial class NumericalAnalysisForm : Form
{
private void btnStartAnalysis_Click(object sender, EventArgs e)
{
txtAnalysisResult.Text = "Please wait while analysing the population.";
txtAnalysisResult.Text = new AnalysisEngine().AnalyzePopulation().ToString();
}
}
class AnalysisEngine
{
public int AnalyzePopulation()
{
Thread.Sleep(4000);
return new Random().Next(1, 5000);
}
}
Using Background Thread
After analyzing the code, you realized that since the main thread is performing all the calculations, it is not getting the opportunity to keep the application responsive. For the same reason, even the text "Please wait while analyzing the population." is not appearing on the screen. Hence you decided to refactor the code so that it is performing all the calculation on a background thread. Once the calculation is finished, you display the result as required. Following is the refactored code:
public partial class NumericalAnalysisForm : Form
{
private void btnStartAnalysis_Click(object sender, EventArgs e)
{
int result = 0;
txtAnalysisResult.Text = "Please wait while analysing the population.";
var analysisTask = Task.Run(() =>
{
result = new AnalysisEngine().AnalyzePopulation();
});
analysisTask.Wait();
txtAnalysisResult.Text = result.ToString();
}
}
class AnalysisEngine
{
public int AnalyzePopulation()
{
Thread.Sleep(10000);
return new Random().Next(1, 5000);
}
}
During the test, you found that application is still not responsive as required even though calculation is happening on a background thread. The reason is quite obvious. The main thread is in a blocked state (waiting for the task to be completed) and not getting the opportunity to keep the application responsive. Hence you further refactored the code to make sure the main thread is not in a blocked state while background thread is calculating. Here is the refactored code:
public partial class NumericalAnalysisForm : Form
{
private void btnStartAnalysis_Click(object sender, EventArgs e)
{
txtAnalysisResult.Text = "Please wait while analysing the population.";
var analysisTask = Task.Run(() =>
{
txtAnalysisResult.Text = new AnalysisEngine().
AnalyzePopulation().ToString();
});
}
}
class AnalysisEngine
{
public int AnalyzePopulation()
{
Thread.Sleep(10000);
return new Random().Next(1, 5000);
}
}
When you run the above application, it remains responsive during the calculation period. However just before displaying the result, an exception "Cross-thread operation not valid: Control 'txtAnalysisResult
' accessed from a thread other than the thread it was created on." is thrown. This exception is thrown because you are accessing a UI control on a background thread. To solve this, you refactored the code as follows:
public partial class NumericalAnalysisForm : Form
{
public NumericalAnalysisForm()
{
InitializeComponent();
}
private void btnStartAnalysis_Click(object sender, EventArgs e)
{
txtAnalysisResult.Text = "Please wait while analysing the population.";
Task.Run(() =>
{
int result = new AnalysisEngine().AnalyzePopulation();
this.BeginInvoke(new MethodInvoker (()=>
{
txtAnalysisResult.Text = result.ToString();
}));
});
}
}
class AnalysisEngine
{
public int AnalyzePopulation()
{
Thread.Sleep(10000);
return new Random().Next(1, 5000);
}
}
With this refactored piece of code, everything looks ok. The application remains responsive during the calculation and the result is displayed properly on a text box. During the review process, the reviewer highlighted that AnalyzePopulation()
method is being called from several areas of the codebase. To ensure that the application remains responsive in all those areas, the above piece of code needs to be duplicated. During the discussion with him, you agreed that AnalysisEngine
class should be refactored so it exposes an asynchronous version of AnalyzePopulation()
method which can be used in all the places to minimize duplication of code. After the review, you refactored the code as follows:
public partial class NumericalAnalysisForm : Form
{
private void btnStartAnalysis_Click(object sender, EventArgs e)
{
txtAnalysisResult.Text = "Please wait while analysing the population.";
new AnalysisEngine().AnalyzePopulationAsync((result, exception) =>
{
txtAnalysisResult.Text = exception != null ?
exception.Message : result.ToString();
});
}
}
class AnalysisEngine
{
public void AnalyzePopulationAsync(Action<int, /> callBack)
{
var context = SynchronizationContext.Current ?? new SynchronizationContext();
Task.Run(() =>
{
try
{
int result = AnalyzePopulation();
context.Send((ignore) => callBack(result, null), null);
}
catch (Exception exp)
{
context.Send((ignore) => callBack(int.MinValue, exp), null);
}
});
}
public int AnalyzePopulation()
{
Thread.Sleep(10000);
return new Random().Next(1, 5000);
}
}
In this version of refactoring, you have exposed a generic asynchronous version of AnalyzePopulation()
method which gives a call back to caller with result and exception. The callback happened in the same synchronization context as provided by the caller. In this case example, call back will happen on the GUI thread. If there is no synchronization context provided by the caller, a blank synchronization is created. E.g. If AnalyzePopulationAsync
method is called by the console client, then call back will happen in the same background thread which executed the computation. If you have ever worked on a typical UI application, chances are that you must have encountered a similar problem and must have resolved nearly in a similar fashion.
Using Async / Await
With .NET 4.5, Microsoft has extended the language and BCL to provide a much simple and cleaner approach to resolve such issues. Following is the refactored code using async
/ await
keywords.
public partial class NumericalAnalysisForm : Form
{
public NumericalAnalysisForm()
{
InitializeComponent();
}
private async void btnStartAnalysis_Click(object sender, EventArgs e)
{
txtAnalysisResult.Text = "Please wait while analysing the population.";
try
{
int result = await new AnalysisEngine().AnalyzePopulationAsync();
txtAnalysisResult.Text = result.ToString();
}
catch (System.Exception exp)
{
txtAnalysisResult.Text = exp.Message + Thread.CurrentThread.Name;
}
}
}
class AnalysisEngine
{
public Task<int /> AnalyzePopulationAsync()
{
Task<int /> task = new Task<int />(AnalyzePopulation);
task.Start();
return task;
}
public int AnalyzePopulation()
{
Thread.Sleep(10000);
return new Random().Next(1, 5000);
}
}
With the above code, we achieve the same results: the application remains responsive and the result is displayed properly. Here are the details how we managed to get the same results:
- We refactored the
AnalysisEngine
class so AnalyzePopulationAsync()
method is returning a task. The newly created task is executing the AnalyzePopulation()
method in a background thread. - We added "
async
" keyword in btnStartAnalysis_Click
method signature. It is an indication to the compiler so that the compiler can refactor the code during compilation. - Within
btnStartAnalysis_Click
method, we are calling the AnalyzePopulationAsync()
method with await
keyword. This indicates to the compiler that:
- The current thread should immediately return from here
- Whatever statements are after this point needs to be executed once the task returned by
AnalyzePopulationAsync
complete and needs to be executed in the current synchronization context.
With these indications, the compiler has enough information that it can refactor btnStartAnalysis_Click
method during compilation so that the main thread will not be blocked and once task completes, the result will be displayed using the main thread. The interesting part is that code generated by compiler will nearly be same as what you refactored earlier. The only difference is that this time compiler is doing it for you and your code will look more clean and concise.
Few Things to be Noted
- Like your implementation, the compiler generated code will execute the statements after
await
statement in the same task thread if there is no synchronization context. - If exception occurred in task, it will be thrown immediately after
await
statement in the same synchronization context. Async
/Await
is just an indication to the compiler so it can refactor the code. It has no significance in the runtime.
Summary
Async
is an important new feature in the .NET Framework. The async
and await
keywords enable you to provide significant user experience improvements in your applications without much effort on your part. In addition, Microsoft has added a large set of new async
APIs within the .NET Framework that returns task, e.g., HTTPClient
class and StreamReader
class now exposes asynchronous APIs that can be used with async
/await
to keep the application responsive.