Statics
This article discusses two different behaviors that are caused by
static keywords and that can be difficult to troubleshoot. Hopefully after you
read this article, you can avoid these behaviors in your applications and be
able to better diagnose them if they occur.
Users see incorrect data
One symptom that is very difficult to troubleshoot occurs when a
user submits data, but another user's information is displayed or is entered
into a database. This behavior typically occurs because of incorrect use of
static variables in classes. A static object is instantiated one time for each
application domain, and all processing in the application shares the same
object. For example, let's say you set up a static property in a class by using
the following code.
using System;
namespace statics
{
public class BadClass
{
private static string _MyData;
public static string MyData
{
get{return _MyData;}
set{_MyData = value;}
}
}
}
The
MyData property is static and public. You can access it anywhere in your
application by using the following syntax:
BadClass.MyDataSo now imagine that you have a page that sets the value
of
MyData, and then does some work with it. If multiple users hit the page
at the same time, you now have a race condition where the last user who updated
MyData wins. The following sample shows this behavior by setting the
static object during a
button_click event. This sample also makes the thread sleep for 15 seconds.
This allows both requests to run at the same time. To do this, follow these
steps:
- Create an .aspx Web form.
- Add a label, a button, and a text box. The HTML will look
similar to the following.
<body>
<form id="Form1" method="post" runat="server">
<asp:Label id="Label1" runat="server"></asp:Label>
<br>
<asp:Button id="Button1" runat="server" Text="Button"></asp:Button>
</br>
<asp:TextBox id="TextBox1" runat="server"></asp:TextBox>
</form>
</body>
- Double-click the button to create a button_click event.
private void Button1_Click(object sender, System.EventArgs e)
{
//Set the static property to the value of the text box.
BadClass.MyData = TextBox1.Text;
//Make the thread wait 30 seconds
System.Threading.Thread.Sleep(30000);
//Set the label to the value of the property.
Label1.Text = BadClass.MyData;
}
- Open two separate Microsoft Internet Explorer processes:
- Click Start, point to All
Programs, and then click Internet
Explorer.
- Repeat step a.
This will ensure that you have two separate Iexplore.exe
processes running at the same time. You can check Task Manager to make sure.
When you have two Iexplore.exe processes running at the same time, you have
unique sessions for each page request. Requests that have the same session ID
are processed in the order that they come in and do not run at the same time.
- Browse your .aspx page in both browser windows.
- Submit User1 in the text box of one
browser window, and then submit User2 in the text box in
the other browser window.
After the pages complete, you will see
User2 in both labels.
As mentioned previously, both pages are running at the same
time. The first request sets the static property to User1, and then goes to
sleep. While the thread is paused, another request comes in and sets the same
object to User2. The thread that is processing the first request completes, and
then displays the value. The value is set to User2.
Avoid this behavior
The best way to avoid this behavior is to not make the object
static and to use an instance of the class to set and to retrieve the values.
The property is no longer shared because a copy of the object is created for
each request, and you will not see this behavior. To avoid this behavior,
follow these steps:
- Remove the static modifier from the BadClass.cs file.
using System;
namespace statics
{
public class BadClass
{
private string _MyData; //Removed static
public string MyData //Removed static
{
get{return _MyData;}
set{_MyData = value;}
}
}
}
- Change the button_click event.
private void Button1_Click(object sender, System.EventArgs e)
{
//Create an instance of BadClass
BadClass _bad = new BadClass();
//Set the MyData property on this instance
_bad.MyData = TextBox1.Text;
//Sleep for 30 seconds
System.Threading.Thread.Sleep(30000);
//Set the label to the value of the property
Label1.Text = _bad.MyData;
}
If you have to use the static object, you have to implement
locking when you are using the static object. This prevents both threads from
accessing the object at the same time. To do this, follow these steps:
- Add a static object to your code behind the page. This
object will be a shared resource that the pages can obtain a lock on.
static object StaticLocker = new object();
- Change the button_click event to the following.
private void Button1_Click(object sender, System.EventArgs e)
{
//Lock the object to prevent multiple threads from writing to the static property
lock(StaticLocker)
{
//Set the static property
BadClass.MyData = TextBox1.Text;
//Put the thread to sleep for 30 seconds
System.Threading.Thread.Sleep(30000);
//Set the label to the value of the static property
Label1.Text = BadClass.MyData;
}
}
NullReferenceException caused by statics
Another behavior that occurs when you use statics is a
System.NullReferenceException exception. For example, you change your
button_click event code to the following.
private void Button1_Click(object sender, System.EventArgs e)
{
BadClass.MyData = TextBox1.Text;
System.Threading.Thread.Sleep(10000);
Label1.Text = BadClass.MyData.ToString();
BadClass.MyData = null;
}
If you perform the same test as before, when
BadClass.MyData.ToString() is called for the second request, it will throw a
System.NullReferenceException exception. The System.NullReferenceException
exception occurs because the first request set the value to null while the
second request was waiting. The resolution for this behavior is the same as for
a static property.
Memory leak caused by static events
If you set up a static event and then subscribe to that event
from an .aspx page, the process will eventually run out of memory. Let's
consider you add the following to the BadClass.cs file.
public delegate void MyEvent();
public static event MyEvent BadEvent;
You then wire up the event delegate in the
InitializeComponent method of the .aspx page to a method in the page.
private void InitializeComponent()
{
//Add this with the other items (for example, this.Load).
BadClass.BadEvent += new BadClass.MyEvent(this.TestEvent);
}
You also add the following method to your page. This is the method that
you specified when hooking up the event in the
InitializeComponent method:
private void TestEvent()
{
//Do some work here.
}
Why this causes a memory leak
Because the event is static and never goes out of scope, you are
adding the method to the list of events that are fired when the event occurs
each time the page is run. The end result of this is that whatever object you
wire to the static event is rooted in memory, and it will never be collected.
In this case, that object is the actual .aspx page class.
This
behavior occurs in the Windbg.exe debugger or the Cdb.exe debugger by running
the
!gcroot command on the object that you are adding the event from. You
will see output that is similar to the following.
HANDLE(Strong):c3d11d8:Root:0x90b8838(System.Object[])->0x5346f68(statics.BadClass/MyEvent)->
0x10ff928(statics.BadClass/MyEvent)->
0x10faa24(statics.BadClass/MyEvent)->
Many object requests have been made to the following events.
0x10e6fe0(statics.BadClass/MyEvent)->
0x533baf0(ASP.StaticsTest_aspx)
You can see that the bottom item is the .aspx page class, and it is
rooted to multiple MyEvent delegates. Because the .aspx page is rooted, it
cannot be cleaned up. The other side effect is that whatever the .aspx page is
using also cannot be cleaned up because they are rooted in the page
object.
For more information about the Microsoft .NET garbage
collector, visit the following MSDN Web sites:
For more information about the
!gcroot command and obtaining this output, see the ".NET CLR Memory
Counters" section of the following MSDN Web site:
Avoid this behavior
To avoid this behavior, you can either not use the static keyword
on the event or remove the event handler from the page when you are done using
it. In an ASP.NET example, you wire the event when
Page_Init is called. You must remove it when the page unloads by adding an
event handler for the
Page_Unload event.
Add the following to the
InitializeComponents method.
this.Unload += new System.EventHandler(this.Page_Unload);
Add the following method to the page to remove the event:
private void Page_Unload(object sender, System.EventArgs e)
{
BadClass.test -= new BadClass.MyEvent(this.TestEvent);
}
For more information about events, visit the following MSDN Web site: