Introduction
In this article, I am going to demonstrate few techniques for tackling multithreading problems into the ASP.NET world. The content and code snippets are applicable to both major frameworks that are built on top of the ASP.NET core platform - ASP.NET MVC and ASP.NET Web Forms.
ASP.NET platform is multithreaded by its nature and provides programming models that shield us from the complexity of using threads. But then, why do we need to care what is under the hood and what is going on with all the threads in a web application? When and why do we need to be aware of the threading model of the platform? How to utilize it properly? I'll try to answer these questions here but let's set the scene first with a very quick overview of the main concepts that we need to understand before we delve into multithreading and asynchronous programming in the ASP.NET world.
Every ASP.NET web application has its own pool of threads that is used for serving requests. Limitations of this pool can be configured on web server level as well as application level. But as you noticed from the last sentence, there are limitations and more threads are not necessarily equal to better and faster application. When request hits an ASP.NET web application, a thread from this pool is pulled out and is used for processing it throughout the framework pipeline. As long as a thread is processing, it cannot serve another request. Let me describe you a scary picture. Imagine a heavy-loaded application with occasional load bursts that lead to thousands of simultaneous requests. If request processing is long enough and the number of requests is large enough, this causes a condition called thread starvation - there are no worker threads available. As a result, IIS starts to queue newly arrived requests and application performance is starting to degrade progressively. When the request queue is full, our web server starts to reject new requests and our application is also known as "down".
In the following paragraphs, I am going to present few scenarios that can lead our application to the condition described above as well as steps that we can take to prevent our application from performing poorly and being even unavailable.
Problem 1
We have an application that displays forecast data. The data is pulled out from weather data provider which exposes HTTP service to communicate with. On every page load of our home page, we need to call the web service and display the fetched data. This problem is an example for blocking I/O request. If we process this request synchronously (on the initially allocated thread from the pool), we are actually blocking a thread pool thread to wait without doing any work. There are mechanisms on operating system level for asynchronous I/O operations that are well incorporated into the .NET APIs and CLR. The recommended approach when we need to do a blocking network or I/O operation in our request and process / display the result on the client side is to use asynchronous controllers or pages (in general, asynchronous HTTP handlers that both major frameworks provide in some form). Check out the code snippets for solving this problem in the section below.
Solution
ASP.NET MVC (4 and above) Snippet
public class WeatherController : Controller
{
private string weatherProviderUrl = "http://ourweatherprovider.com/api/get";
public async Task<ActionResult> Get()
{
using (HttpClient httpClient = new HttpClient())
{
var response = await httpClient.GetAsync(this.weatherProviderUrl);
string jsonResp = await response.Content.ReadAsStringAsync();
ForecastVM forecastModel =
await JsonConvert.DeserializeObjectAsync<ForecastVM>(jsonResp);
return View(forecastModel);
}
}
}
Code Comment
The code above fetches all the data from the weather web service and creates a view model with it that is passed to the view. All the network work is done asynchronously which solves our blocking and thread wasting for nothing problem. MVC framework is the easiest one to use from my point of view when it comes to using the async programming model.
ASP.NET Web Forms (4.5 and above) Snippet
public partial class Weather : System.Web.UI.Page
{
private string weatherProviderUrl = "http://ourweatherprovider.com/api/get";
protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(GetWeatherData));
}
public async Task GetWeatherData()
{
using (HttpClient httpClient = new HttpClient())
{
var response = await httpClient.GetAsync(this.weatherProviderUrl);
string jsonResp = await response.Content.ReadAsStringAsync();
ForecastVM forecastModel =
await JsonConvert.DeserializeObjectAsync<ForecastVM>(jsonResp);
this.ourControl.DataSource = forecastModel;
this.ourControl.DataBind();
}
}
}
Don't forget
<%@ Page Async="true" Language="C#" AutoEventWireup="true"
CodeBehind="Weather.aspx.cs" Inherits="WebThreadingSample.Weather" %>
Code Comment
Since ASP.NET 4.5 asynchronous operations in a page are made a lot easier. The code above executes the same operations as the MVC code and gives practically the same result in terms of functionality. Databinding code is only for completeness of the sample. There are even cooler features (ASP.NET 4.6) like async model binding that are part of the framework now but they are beyond the scope of this article.
Asynchronous HTTP Handler (ASP.NET 4.5 and above) Snippet
public class WeatherHandler : HttpTaskAsyncHandler
{
private string weatherProviderUrl = "http://ourweatherprovider.com/api/get";
public override async System.Threading.Tasks.Task
ProcessRequestAsync(HttpContext context)
{
using (HttpClient httpClient = new HttpClient())
{
var response = await httpClient.GetAsync(this.weatherProviderUrl);
string jsonResp = await response.Content.ReadAsStringAsync();
context.Response.ContentType = "application/json; charset=utf-8";
context.Response.Write(jsonResp);
}
}
}
Code Comment
The code above provides the same result as the other samples, the only difference is that the handler returns JSON response that can be used in Ajax request for example. HTTP handlers are useful in numerous cases which I am not going to discuss in the current text. Scott Hanselman has a great article about them here.
General rule that I tend to follow is to use asynchronous programming in my applications when the ASP.NET thread that serves the request is blocked doing nothing.
Problem 2
We have an application which is used in a hospital by doctors and hospital administrators for managing patients. When a doctor registers a patient into the system, hospital administrator receive an email that registration occurred. If email sending is unsuccessful, the message is added to a queue in the database and sent again in 5 minutes. This problem is again an example for blocking operation but here the user is not interested in the result from this operation and does not need to wait for it. The code snippet below is applicable to both major web frameworks.
Solution
HostingEnvironment.QueueBackgroundWorkItem
(ct => this.notificationService.SendRegisterNotificationsAsync(usersToNotify, patientID));
Code Comment
The sample above is a "fire & forget" technique. It can be used in any controller or page when we do not care about the result in the current request and all the processing and failure handling is done somewhere else in our application. The code executes the SendRegisterNotificationsAsync method on another thread from the thread pool and passes list of user ids for the users that will be notified and a patient id of the newly created patient.
This approach is useful in some scenarios but has some limitations and caveats that should be pointed out:
- This solution is available since .NET 4.5.2. There are alternatives which rely on 3rd party libraries that I'll mention below.
- When it's time for recycling the AppDomain of our application or maybe unhandled exception that will kill our process occurred somewhere, if our little background task is still running the good thing is that ASP.NET knows about it, the bad thing is that ASP.NET will give it a time frame (90 seconds or less) to finish and if it's not completed, it will be canceled in the middle of the execution. So this is not the most reliable solution for background tasks and should be used wisely depending on the scenario. From my point of view, it has a good balance between simplicity and reliability.
- There are alternatives like HangFire - HangFire which are far more sophisticated and complex solutions.
- We can use the
Task.Run
or ThreadPool.QueueUserWorkItem
approach and run our method on another thread from the pool, but this is the least reliable solution because ASP.NET does not know at all that some working is currently executing and can recycle the AppDomain any time without any notification.
A more thorough article on this problem can be found here.
Problem 3
Let's go back to problem 1 - the application that displays forecast data which is collected regularly from a web service. But now we are going to implement a different approach. Our weather data will be collected on a regular interval and stored into the ASP.NET Cache and then retrieved on every request. The implementation below is simplistic and far from perfect but it's useful in certain scenarios where we need simple, in-process, not mission critical background jobs.
Solution
public class BackgroundJob : IRegisteredObject, IDisposable
{
private bool disposed = false;
private System.Threading.Timer recurringTimer;
private Action work;
private int intervalInMs;
private CancellationTokenSource cts;
private Task workTask = null;
private const int TIMEOUT_TO_KILL = 10000;
public BackgroundJob(Action work, int intervalInSeconds, bool runImmediately = true)
{
this.work = work;
this.intervalInMs = intervalInSeconds * 1000;
this.cts = new CancellationTokenSource();
this.recurringTimer = new System.Threading.Timer(Callback, null,
runImmediately ?
1 : this.intervalInMs,
Timeout.Infinite);
HostingEnvironment.RegisterObject(this);
}
private async void Callback(object state)
{
try
{
this.workTask = new Task(() => work.Invoke());
this.workTask.Start();
var ct = this.cts.Token;
await this.workTask.WithCancellation(ct);
}
catch(OperationCanceledException)
{ }
finally
{
this.recurringTimer.Change(this.intervalInMs, Timeout.Infinite);
}
}
public void Stop(bool immediate)
{
try
{
if (this.workTask != null && this.workTask.Status == TaskStatus.Running)
{
this.cts.CancelAfter(TIMEOUT_TO_KILL);
this.workTask.Wait();
}
}
finally
{
HostingEnvironment.UnregisterObject(this);
}
}
}
Here is the little extension helper that I am using in the code above for cancelling tasks.
public static class TaskExtensions
{
public static async Task WithCancellation
(this Task originalTask, CancellationToken ct)
{
var cancelTask = new TaskCompletionSource<object>();
using (ct.Register(t =>
((TaskCompletionSource<object>)t).TrySetResult(new object()), cancelTask))
{
Task any = await Task.WhenAny(originalTask, cancelTask.Task);
if (any == cancelTask.Task)
ct.ThrowIfCancellationRequested();
}
await originalTask;
}
}
Code Comment
An instance of the BackgroundJob class must be alive as long as the AppDomain is alive in order for your task to be repeated. You can use the instance as a static member in global.asax and create it on application_start or take any other approach for saving application state and running application startup code.
This approach has similar limitations to Problem 2. We need to be aware of AppDomain recycling events and unhandled exceptions. My recommendation is to use out of process solutions
that are not part of the ASP.NET application for critical background tasks.There are far more sophisticated solutions than the one presented here but it is all about what problem we are trying to solve. Problem 2 and Problem 3 are very similar from technical point of view and we can combine them and simplify even more the above code. But from "real world" point of view, I think they solve two different cases each having different problems to think about. Great articles on this topic can be found
here &
here.
Sample App
Following community request, I have updated the article with sample ASP.NET MVC application which demonstrates the use of the BackgroundJob
class. You can see how the background task is initialized in the Global.asax file. IncrementState
method is the actual task that is repeatedly executed.
public class MvcApplication : System.Web.HttpApplication
{
internal static int State { get; set; }
private static BackgroundJob recurringJob;
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
recurringJob = new BackgroundJob(this.IncrementState, 3);
}
private void IncrementState()
{
Random rand = new Random();
int nextRand = rand.Next();
State = nextRand;
}
}
The State
property is used into the Index
action of the Home controller and passed to the view via the ViewBag
.
public ActionResult Index()
{
this.ViewBag.IntState = MvcApplication.State;
return View();
}
You can actually see the background task changing the State
property when refreshing the /Home/Index page of the application.
Conclusion
As a summary of all the above, I'll try to extract few general rules that ASP.NET developer can safely follow and be careful when breaking.
- Use asynchronous code when you have I/O operations that should be completed as part of serving a request. Code snippets from the first sample problem are perfect for this.
- Avoid queueing background work on the ASP.NET thread pool. If you have your reasons to do it, use the approach showed in the second sample problem as a bare minimum.
- If you run in process background tasks in your ASP.NET application, be aware and ready for potential interruptions and work loss, hence be very selective when choosing what kind of task to implement in this manner.
History
- 29th December, 2015: Article created
- 23rd January, 2016: Sample ASP.NET MVC application added showing the use of the
BackgroundJob
class