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,

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Threading;

namespace RaceConditionInAsyncController.Controllers
{
    public class HomeController : AsyncController
    {
        Thread th1;
        Thread th2;
        public void IndexAsync()
        {
            AsyncManager.OutstandingOperations.Increment(2);
            th1 = new Thread(new ThreadStart(DoLong1));
            th1.Start();
            th2 = new Thread(new ThreadStart(DoLong2));
            th2.Start();
        }
        void DoLong1()
        {
            DateTime dt = DateTime.Now;
            string str = "---Task 1 Started at: " + dt.ToString();
            System.Threading.Thread.Sleep(5000);
            str += "<br>---Task 1 Stopped at: " + DateTime.Now.ToString();
            AsyncManager.Parameters["message1"] = str;
            AsyncManager.OutstandingOperations.Decrement();
        }
        void DoLong2()
        {
            DateTime dt = DateTime.Now;
            string str = "*****Task 2 Started at: " + dt.ToString();
            System.Threading.Thread.Sleep(5000);
            str += "<br>*******Task 2 Stopped at: " + DateTime.Now.ToString();
            AsyncManager.Parameters["message2"] = str;
            AsyncManager.OutstandingOperations.Decrement();
        }
        public ActionResult IndexCompleted(string message1, string message2)
        {
            ViewData["message1"] = message1;
            ViewData["message2"] = message2;

            return View();
        }
    }
}

 

          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,

 

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 

Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">    
 <b>Task1:</b> <%= ViewData["message1"] %>
    <hr />
    <br /><b>Task2:</b> <%= ViewData["message2"] %>
</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,

 

public TValue this[TKey key]
{
        get
        {
            int index = this.FindEntry(key);
            if (index >= 0)
            {
                return this.entries[index].value;
            }
            ThrowHelper.ThrowKeyNotFoundException();
            return default(TValue);
        }
        set
        {
            this.Insert(key, value, false);
        }
}


private int FindEntry(TKey key)
{
        if (key == null)
        {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
        }
        if (this.buckets != null)
        {
            int num = this.comparer.GetHashCode(key) & 0x7fffffff;
            for (int i = this.buckets[num % this.buckets.Length]; i >= 0; i = this.entries[i].next)
            {
                if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
                {
                    return i;
                }
            }
        }
        return -1;
}

 

           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,

 

public TValue this[TKey key]
{
        get
        {
            int index = this.FindEntry(key);
            if (index >= 0)
            {
                return this.entries[index].value;
            }
            ThrowHelper.ThrowKeyNotFoundException();
            return default(TValue);
        }
        set
        {
            this.Insert(key, value, false);
        }
}

private void Insert(TKey key, TValue value, bool add)
{
        int freeList;
        if (key == null)
        {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
        }
        if (this.buckets == null)
        {
            this.Initialize(0);
        }
        int num = this.comparer.GetHashCode(key) & 0x7fffffff;
        int index = num % this.buckets.Length;
        for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
        {
            if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
            {
                if (add)
                {
                    ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
                }
                this.entries[i].value = value;
                this.version++;
                return;
            }
        }
        if (this.freeCount > 0)
        {
            freeList = this.freeList;
            this.freeList = this.entries[freeList].next;
            this.freeCount--;
        }
        else
        {
            if (this.count == this.entries.Length)
            {
                this.Resize();
                index = num % this.buckets.Length;
            }
            freeList = this.count;
            this.count++;
        }
        this.entries[freeList].hashCode = num;
        this.entries[freeList].next = this.buckets[index];
        this.entries[freeList].key = key;
        this.entries[freeList].value = value;
        this.buckets[index] = freeList;
        this.version++;
}

 

           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.

1 Comment

Comments have been disabled for this content.