Asynchrony in .NET World

Kshitiz Shakya
6 min readSep 24, 2020

During my first job as a C# Software Developer, I was required to build custom reports in a .NET WinForms Application with data from MSSQL Server. I was relatively inexperienced back then and was just building it by duplicating and making changes to existing reports in the codebase. A noticeable issue in that application was that loading some reports made the User Interface (UI) unresponsive. A quick google search made me aware about the cause and introduced me to the concept of asynchronous programming.

So, what made the application unresponsive?

The data for the reports were prepared via a query to the database. Since the main UI thread was preparing the reports (which could take a while), it froze when waiting for the long running step to complete.

The query was as optimized as possible and completed in 8–10 seconds, which was fine. I just wanted to prevent the application from freezing.

And how to prevent the application from freezing?

The answer: Make it asynchronous!!

Understanding asynchrony

Let us first understand what asynchrony is. When talking about asynchrony, many developers get it confused with multithreading. Although multithreading is an important part of the typical use of asynchrony, the two are not the same.

To understand the difference, consider this excellent analogy from this post in stackoverflow.

You are cooking in a restaurant. An order comes in for eggs and toast.

Synchronous: you cook the eggs, then you cook the toast.

Asynchronous, single threaded: you start the eggs cooking and set a timer. You start the toast cooking, and set a timer. While they are both cooking, you clean the kitchen. When the timers go off you take the eggs off the heat and the toast out of the toaster and serve them.

Asynchronous, multithreaded: you hire two more cooks, one to cook eggs and one to cook toast. Now you have the problem of coordinating the cooks so that they do not conflict with each other in the kitchen when sharing resources. And you have to pay them.

When we talk about threads, we are concerned with workers. But in the case of asynchrony, it is about tasks and continuations.

Implementing asynchrony in .NET World

There are three approaches to achieve asynchrony in the .NET World.

  1. The BeginInvoke/ EndInvoke approach.
  2. Using BackgroundWorker for event-based asynchronous pattern.
  3. The Task Parallel Library.

We will implement all these approaches using a simple WinForms Application, that just loads some arbitrary report — loading the report takes a while — by clicking some buttons. You can download the application from here.

On running the application, you will see a window as shown below (apology for the bad UI design). There are 4 buttons that will load the report. The first button ‘Load Report (Synchronous)’ will load the report synchronously, meaning the application will freeze for some time, before showing the results in the ListView. Just try moving the window when loading the report. The rest of the buttons will load the report asynchronously using the 3 approaches that we discussed.

1. The BeginInvoke/ EndInvoke approach

This approach is from the .NET 1.x, using IAsyncResult and AsyncCallback to propagate the result. The button 'Load Report(BeginInvoke / EndInvoke)' loads the report using this method. See the region AsynchronyUsingIAsyncResult in Form1.cs

Here is the code snippet that loads the report using this method. Here is the code snippet that loads the report using this method. Even though this accomplishes the goal of preventing the UI from freezing, it is difficult to understand due to the use of delegates.

private delegate ListViewItem[] AsyncMethodCaller();

private void btnLoadReportInvoke_Click(object sender, EventArgs e)
{
listView1.Items.Clear();
Application.DoEvents();
AsyncMethodCaller caller = new AsyncMethodCaller(GetLongRunningProductsReport);
caller.BeginInvoke(new AsyncCallback(UpdateListViewCallback), null);
}

private void UpdateListViewCallback(IAsyncResult result)
{
AsyncResult ar = (AsyncResult)result;
AsyncMethodCaller caller =(AsyncMethodCaller)ar.AsyncDelegate;
var records = caller.EndInvoke(result);
listView1.BeginInvoke(new InvokeDelegate(UpdateListView), new object[] { records });
}

private delegate void InvokeDelegate(ListViewItem[] records);

private void UpdateListView(ListViewItem[] records)
{
listView1.Items.AddRange(records);
}

private ListViewItem[] GetLongRunningProductsReport()
{
var collections = new ListViewItem[100];
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
var product = new Product()
{
Sku = $"Product_{i}",
Name = $"ProductName_{i}",
Description = $"ProductDescription_{i}",
Cost = rand.Next(1, 1000),
Price = rand.Next(1, 1000)
};
var listViewItem = new ListViewItem(product.Sku);
listViewItem.SubItems.Add(product.Name);
listViewItem.SubItems.Add(product.Description);
listViewItem.SubItems.Add($"${product.Cost.ToString()}");
listViewItem.SubItems.Add($"${product.Price.ToString()}");
collections = listViewItem; ;
}
return collections;
}

First, we use the delegate AsyncMethodCaller which matches the signature of the method GetLongRunningProductsReport. Then, we call the BeginInvoke of the delegate passing in the callback AsyncCallback that invokes the method UpdateListViewCallback. Calling the BeginInvoke initiates the long running method in an asynchronous way. The result of the method is obtained through the IAsyncResult object by the call EndInvoke in the callback method. Once the result is available, the ListView is updated with the records in the proper thread.

In short, the main idea here is to call the long running method GetLongRunningProductsReport asynchronously and registering a callback AsyncCallback that invokes the method UpdateListViewCallback once the asynchronous operation completes. The callback handles updating the ListView in the proper UI thread.

The problem with this code is that it is not very easy to read.

2. Using BackgroundWorker

A better approach was introduced in .NET 2.0, using the BackgroundWorker to offload long running task to the background. This frees up the main UI thread, preventing it from freezing. The button 'Load Report(BackgroundWorker)' loads the report using this method. See region BackgroundWorker in Form1.cs.

BackgroundWorker worker = new BackgroundWorker();

worker.DoWork += Worker_DoWork;

private void btnLoadReportBackground_Click(object sender, EventArgs e)
{
listView1.Items.Clear();
Application.DoEvents();
worker.RunWorkerAsync();
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var records = GetLongRunningProductsReport();
listView1.BeginInvoke(new InvokeDelegate(UpdateListView), new object[] { records });
}

private delegate void InvokeDelegate(ListViewItem[] records);

private void UpdateListView(ListViewItem[] records)
{
listView1.Items.AddRange(records);
}


private ListViewItem[] GetLongRunningProductsReport()
{
var collections = new ListViewItem[100];
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
var product = new Product()
{
Sku = $"Product_{i}",
Name = $"ProductName_{i}",
Description = $"ProductDescription_{i}",
Cost = rand.Next(1, 1000),
Price = rand.Next(1, 1000)
};
var listViewItem = new ListViewItem(product.Sku);
listViewItem.SubItems.Add(product.Name);
listViewItem.SubItems.Add(product.Description);
listViewItem.SubItems.Add($"${product.Cost.ToString()}");
listViewItem.SubItems.Add($"${product.Price.ToString()}");
collections = listViewItem; ;
}
return collections;
}

First, we create an instance of BackgroundWorker and hook its DoWork event with the handler Worker_DoWork. This handler is responsible for getting the results from the long running method GetLongRunningProductsReport and updating ListView (in the proper thread) with the results. When the RunWorkerAsync is called (on button click), the Worker_DoWork is called in the background and everything works as expected.

This is a more readable approach and the one I used to fix the problem in my first job.

3. The Task Parallel Library

The Task Parallel Library (TPL) was introduced in .NET 4 and expanded on .NET 4.5. Despite its excellent design, writing readable, robust code with the TPL was still hard. Then, with the release of C# 5, it was now possible to write readable synchronous-looking code that uses asynchrony where appropriate using the async / await language constructs. Gone were the days of the spaghetti callbacks and BackgroundWorker subscriptions. This allowed a more succinct and elegant approach that even new developers could easily pick up.

The button ‘Load Report (async / await)’ loads report using this approach. See region async/await in Form1.cs.

private async void btnLoadReportTPL_Click(object sender,
EventArgs e)
{
listView1.Items.Clear();
Application.DoEvents();
var result = await GetLongRunningProductsReportAsync();
listView1.Items.AddRange(result);
}

private async Task<ListViewItem[]> GetLongRunningProductsReportAsync()
{
var collections = new ListViewItem[100];
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
var product = new Product()
{
Sku = $"Product_{i}",
Name = $"ProductName_{i}",
Description = $"ProductDescription_{i}",
Cost = rand.Next(1, 1000),
Price = rand.Next(1, 1000)
};
var listViewItem = new ListViewItem(product.Sku);
listViewItem.SubItems.Add(product.Name);
listViewItem.SubItems.Add(product.Description);
listViewItem.SubItems.Add($"${product.Cost.ToString()}");
listViewItem.SubItems.Add($"${product.Price.ToString()}");
collections = listViewItem;
}
return collections;
}

Notice the use of async, await, Task in the above code snippet. The main purpose of the await is to avoid blocking while you wait for a long running operation to complete. async just tells the compiler that the method can be awaited. Task is the central concept from the TPL, which represents an asynchronous operation. With just minor modifications, a synchronous looking code can be made asynchronous using this approach.

This is now the de-facto way for asynchronous programming in the .NET World. As such, I encourage every .NET developer to learn about async/await language constructs and the Task Parallel Library. Here are some resources to help you get started:

Originally published at https://kshitizshakya.com.np on September 24, 2020.

--

--