Race Condition in AsyncController
Introduction:
ASP.NET MVC 2
provides AsyncController which enables you to define
controller actions that run asynchronously. AsyncController
become very useful in IO bound tasks, for example calling a
remote web service or query large amounts of data from a
slow database, etc. In these situation you will not tie up a
request processing thread(ASP.NET thread), instead you use
AsyncController which enables you to use non-ASP.NET
thread to execute long IO bound operations. AsyncController
will increase overall application performance if your
application is using long running IO bound task.
Synchronization is very important if you
are performing multiple task in AsyncController, but if you
don't care then your application may come into a situation
that we refer to as a race condition. Race condition occurs
when several threads attempt to access the same data and do
not take account of what the other threads are doing which
result in corrupt data structures. So in this article I will
show you how your application may come into race condition
situation and how you can avoid race condition situation.
Description:
How does the race condition occur in AsyncController is best explained with example. I am taking the example from this thread. Create a sample ASP.NET MVC application. Then open HomeController.cs and add the following code,
01 |
using
System;
|
02 |
using
System.Collections.Generic;
|
03 |
using
System.Linq;
|
04 |
using
System.Web;
|
05 |
using
System.Web.Mvc;
|
06 |
using
System.Threading;
|
07 |
08 |
namespace
RaceConditionInAsyncController.Controllers
|
09 |
{
|
10 |
public
class
HomeController : AsyncController
|
11 |
{
|
12 |
Thread th1;
|
13 |
Thread th2;
|
14 |
public
void
IndexAsync()
|
15 |
{
|
16 |
AsyncManager.OutstandingOperations.Increment(2);
|
17 |
th1 = new
Thread(new
ThreadStart(DoLong1));
|
18 |
th1.Start();
|
19 |
th2 = new
Thread(new
ThreadStart(DoLong2));
|
20 |
th2.Start();
|
21 |
}
|
22 |
void
DoLong1()
|
23 |
{
|
24 |
DateTime dt = DateTime.Now;
|
25 |
string
str = "---Task 1 Started at: "
+ dt.ToString();
|
26 |
System.Threading.Thread.Sleep(5000);
|
27 |
str += "<br>---Task 1 Stopped at: "
+ DateTime.Now.ToString();
|
28 |
AsyncManager.Parameters["message1"] = str;
|
29 |
AsyncManager.OutstandingOperations.Decrement();
|
30 |
}
|
31 |
void
DoLong2()
|
32 |
{
|
33 |
DateTime dt = DateTime.Now;
|
34 |
string
str = "*****Task 2 Started at: "
+ dt.ToString();
|
35 |
System.Threading.Thread.Sleep(5000);
|
36 |
str += "<br>*******Task 2 Stopped at: "
+ DateTime.Now.ToString();
|
37 |
AsyncManager.Parameters["message2"] = str;
|
38 |
AsyncManager.OutstandingOperations.Decrement();
|
39 |
}
|
40 |
public
ActionResult IndexCompleted(string
message1, string
message2)
|
41 |
{
|
42 |
ViewData["message1"] = message1;
|
43 |
ViewData["message2"] = message2;
|
44 |
45 |
return
View();
|
46 |
}
|
47 |
}
|
48 |
}
|
I am using Thread class to spawn thread only for demonstration. I am not recommending to use Thread class to create threads. Next open Index view of HomeController and add the following lines,
01 |
<%@ Page Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
|
02 |
03 |
Inherits="System.Web.Mvc.ViewPage"
%>
|
04 |
05 |
<asp:Content
ID="Content1"
ContentPlaceHolderID="TitleContent"
runat="server">
|
06 |
Home Page
|
07 |
</asp:Content>
|
08 |
09 |
<asp:Content
ID="Content2"
ContentPlaceHolderID="MainContent"
runat="server">
|
10 |
<b>Task1:</b> <%= ViewData["message1"] %>
|
11 |
<hr
/>
|
12 |
<br
/><b>Task2:</b> <%= ViewData["message2"] %>
|
13 |
</asp:Content>
|
Just run this application many times. You
will find that most of times your view is rendered as,
Task1: ---Task 1 Started at:
8/3/2008 2:54:13 AM
---Task 1 Stopped at:
8/3/2008 2:54:18 AM
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Task2: *****Task 2 Started at:
8/3/2008 2:54:13 AM
*******Task 2 Stopped at:
8/3/2008 2:54:18 AM
Some times your view is rendered as,
Task1:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Task2: *****Task 2 Started at:
8/3/2008 2:56:20 AM
*******Task 2 Stopped at:
8/3/2008 2:56:25 AM
Some times your view is rendered as,
Task1: ---Task 1 Started at:
8/3/2008 2:57:11 AM
---Task 1 Stopped at:
8/3/2008 2:57:16 AM
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Task2:
It shows that sometimes ViewData["message1"] is null and sometimes ViewData["message2"] is null. Now, let's dig into code and see what is happening.
You might first think that this is due to one of the thread method is not executed. For testing this just put a break point in IndexCompleted method and then right click on this break point and select Condition,
Add this condition in textbox, string.IsNullOrEmpty(message1) || string.IsNullOrEmpty(message2),
Now just run the application and refresh the browser continuously until the break point hits. When break point hits you will see the following screen.
Just see the QuickWatch window in the above figure. It shows that AsyncManager.Parameters contains 2 key objects(with their Keys message1 and message2), which was added during the execution of above thread methods. This shows that both thread action methods are executed.
The above figure also shows that message1 contains a value and message2 is null. Also see at bottom(Watch window) you will find that AsyncManager.Parameters["message1"] returns a string while AsyncManager.Parameters["message2"] is throwing an exception of type KeyNotFoundException. This is where strange things happen. Just see the above figure again you will find that AsyncManager.Parameters contains two Values and two Keys(one key is message1 and other is message2), but still AsyncManager.Parameters["message2"] throws an exception saying that key is not found. For understanding this you need to see what will happen when you add a key object to AsyncManager.Parameters and when you get a key object from AsyncManager.Parameters.
If you see clearly in the above thread methods code you will find that two different key objects are added in AsyncManager.Parameters in different thread methods. You may think that this is not a problem because different key objects are added in different thread methods. For testing this let first see what happens when you get a key object. Let's say you need to get AsyncManager.Parameters["message1"]. Here is the code(from Dictionary generic class) used to get a key object,
01 |
public
TValue this[TKey key]
|
02 |
{
|
03 |
get
|
04 |
{
|
05 |
int
index = this.FindEntry(key);
|
06 |
if
(index >= 0)
|
07 |
{
|
08 |
return
this.entries[index].value;
|
09 |
}
|
10 |
ThrowHelper.ThrowKeyNotFoundException();
|
11 |
return
default(TValue);
|
12 |
}
|
13 |
set
|
14 |
{
|
15 |
this.Insert(key, value, false);
|
16 |
}
|
17 |
}
|
18 |
19 |
20 |
private
int
FindEntry(TKey key)
|
21 |
{
|
22 |
if
(key == null)
|
23 |
{
|
24 |
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
|
25 |
}
|
26 |
if
(this.buckets != null)
|
27 |
{
|
28 |
int
num = this.comparer.GetHashCode(key) &
0x7fffffff;
|
29 |
for
(int
i = this.buckets[num % this.buckets.Length]; i >= 0; i = this.entries[i].next)
|
30 |
{
|
31 |
if
((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
|
32 |
{
|
33 |
return
i;
|
34 |
}
|
35 |
}
|
36 |
}
|
37 |
return
-1;
|
38 |
}
|
I don't want to go further deep here but the main point to note here is that the next value of i is this.entries[i].next in the above loop. The entries array is very important. Now let's see the entries array two times, one when message1 and message2 parameters of IndexCompleted are not null and next when at least one of the parameter is null.
Just add another break point with a condition, message1 and message2 are not null, you will see the following screen,
Just remove the above break point and refresh the browser till initial break point hits, you will see the following screen,
The important point to note here is that when application render both Tasks(see at top), then entries[0].next = -1 and entries[1].next = 0 and when application render one Task, then entries[0].next = -1 and entries[1].next = -1. This shows that something going wrong here. This is where race condition come into action. Let's see what happens when the above thread methods set AsyncManager.Parameters["message1"] and AsyncManager.Parameters["message2"]. Here is code (from Dictionary generic class) used to set a key object,
01 |
public
TValue this[TKey key]
|
02 |
{
|
03 |
get
|
04 |
{
|
05 |
int
index = this.FindEntry(key);
|
06 |
if
(index >= 0)
|
07 |
{
|
08 |
return
this.entries[index].value;
|
09 |
}
|
10 |
ThrowHelper.ThrowKeyNotFoundException();
|
11 |
return
default(TValue);
|
12 |
}
|
13 |
set
|
14 |
{
|
15 |
this.Insert(key, value, false);
|
16 |
}
|
17 |
}
|
18 |
19 |
private
void
Insert(TKey key, TValue value, bool
add)
|
20 |
{
|
21 |
int
freeList;
|
22 |
if
(key == null)
|
23 |
{
|
24 |
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
|
25 |
}
|
26 |
if
(this.buckets == null)
|
27 |
{
|
28 |
this.Initialize(0);
|
29 |
}
|
30 |
int
num = this.comparer.GetHashCode(key) &
0x7fffffff;
|
31 |
int
index = num % this.buckets.Length;
|
32 |
for
(int
i = this.buckets[index]; i >= 0; i = this.entries[i].next)
|
33 |
{
|
34 |
if
((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
|
35 |
{
|
36 |
if
(add)
|
37 |
{
|
38 |
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
|
39 |
}
|
40 |
this.entries[i].value = value;
|
41 |
this.version++;
|
42 |
return;
|
43 |
}
|
44 |
}
|
45 |
if
(this.freeCount > 0)
|
46 |
{
|
47 |
freeList = this.freeList;
|
48 |
this.freeList = this.entries[freeList].next;
|
49 |
this.freeCount--;
|
50 |
}
|
51 |
else
|
52 |
{
|
53 |
if
(this.count == this.entries.Length)
|
54 |
{
|
55 |
this.Resize();
|
56 |
index = num % this.buckets.Length;
|
57 |
}
|
58 |
freeList = this.count;
|
59 |
this.count++;
|
60 |
}
|
61 |
this.entries[freeList].hashCode = num;
|
62 |
this.entries[freeList].next = this.buckets[index];
|
63 |
this.entries[freeList].key = key;
|
64 |
this.entries[freeList].value = value;
|
65 |
this.buckets[index] = freeList;
|
66 |
this.version++;
|
67 |
}
|
In the above code think your self what happens when freeList = this.count line is executed by first thread and before executing this.count++ line context switch occur and first thread become queue and next thread started to execute. When second thread execute freeList = this.count line, then both thread will set this.entries[freeList].next to the same value. This is the condition which was shown above that entries array element next value was set to wrong value. This situation is called race condition. This is the reason that why you are not seeing the both Tasks in your view sometimes and AsyncManager.Parameters["message2"] throws an exception of type KeyNotFoundException. So be careful about race condition.
So to avoid race condition in your code you can call AsyncManager.Sync method. This will eliminate the race condition. You can also explicitly lock AsyncManager.Parameters to allow only one thread at a time to access AsyncManager.Parameters .
Summary:
Race conditions can lead to data corruption and may show
unexpected results. In this article I showed how an race
condition occur in AsyncController when two or more threads
attempt to access the same data. I showed this with an
example. Hope you will enjoy this article too. Lastly
special thanks to Rick and Levi for their tips.