Shared Variables in ASP.NET

By: Yorai Aminov

Abstract: Variable types are often misused in ASP.NET applications. This article discusses the various types of shared variables in Delphi ASP.NET applications

    Introduction

ASP.NET applications are multi-threaded, so it is usually a bad idea to use global variables in them. However, it is often necessary to use something similar – variables whose scope is not limited to a single function, method, or class. Delphi supports several types of “shared” variables that can be accessed globally, but these are often misused. Because of the difficulty involved in testing and debugging multi-threaded applications, such mistakes often go unnoticed, and only occur when the application is deployed on a busy web server.

    The Test Program

The easiest way to understand the difference between the various variable types is to see them in action. The program has a single page, default.aspx, showing a single text box and a button. Pushing the button performs the following steps:

  1. Retrieve the initial values of each of the variable types.
  2. Set all variables to the value typed in the text box.
  3. Wait for 10 seconds.
  4. Display the initial, entered, and final values of the variables.

The third step is the crucial one, since it gives us enough time to run a second request while the first request is still being processed, and examine the effect of concurrent requests on the application. The complete source code for the page can be found at the end of this article.

    Variable Types

The program contains several classes, descending from a common ancestor, each demonstrating a different type of variable. All classes descend from the Shared class, which defines a common interface:

type
  Shared = class abstract
  public
    class procedure SetValue(v: Integer); virtual; abstract;
    class function GetValue: Integer; virtual; abstract;
  end;

    Global Variables

Global variables are allocated when the application starts, and persist for the duration of the program. A global variable has only a single instance, and all threads access the same variable. The SharedGlobalVar class uses a global variable to store the value used by the SetValue and GetValue methods.

    Class Variables

.NET doesn’t support global functions. Because class methods and variables can be accessed without instantiating the class, they can be used to provide identical functionality. The SharedClassVar class uses a class variable to store the value used by the SetValue and GetValue methods.

    Thread Variables

Thread variables, declared using the threadvar keyword, are like global variables, but each thread gets a separate copy of the variable. The SharedThreadVar class uses a thread variable to store the value used by the SetValue and GetValue methods.

    Thread Static Variables

Thread static variables are class variables decorated with .NET’s [ThreadStatic] attribute. The attribute causes each thread to get a separate copy of the class variable. The SharedThreadStaticVar class uses such a variable to store the value used by the SetValue and GetValue methods.

    Request Variables

Finally, the SharedRequestVar class uses ASP.NET’s HttpRequest object to store the value used by the SetValue and GetValue methods. An HttpRequest object is created for each ASP.NET request.

Instead of storing the value, the SharedRequestVar class uses a simple field, and creates an instance of itself using the Current method. The method checks the HttpRequest object for an existing instance, and only creates a new instance if a previous one cannot be found:

class function SharedRequest.Current: SharedRequest;
begin
  if Assigned(HttpContext.Current.Items['SharedRequest']) then
    Result := HttpContext.Current.Items['SharedRequest'] as SharedRequest
  else
  begin
    Result := SharedRequest.Create;
    HttpContext.Current.Items['SharedRequest'] := Result;
  end;
end;

class function SharedRequest.GetValue: Integer;
begin
  Result := Current.FValue;
end;

class procedure SharedRequest.SetValue(v: Integer);
begin
  Current.FValue := v;
end;

    Running the Program

To test the program, we open two browser windows, and point them to our test page. Each browser window will post a different value. While a request is running in the first window, we’ll post another value from the second window, and wait for the results:

Hide image
Click to see full-sized image

If we enter “10” in the first window, click the “Run” button, then enter “20” in the second window and click its “Run” button (while the first request is still running), we get the following results:

Hide image
Click to see full-sized image

Green labels represent expected values, while red labels represent bad results. Just by checking the colors, we can already see that both class variables and global variables can’t be used for concurrent requests. Both variable types exhibit the same behavior: clicking the “Run” button on the second window changed the values used by the first request, while it was running.

On the other hand, the thread-specific variables seem to have worked just fine. On both requests, these variables start with a value of 0, and end up with the correct value entered in the text box. If we execute another request in the first window, however, we discover a problem:

Hide image
Click to see full-sized image

This time, the initial values of the thread-specific variables are no longer 0. This is because ASP.NET re-uses threads. In this case, the thread used to run the second request was used again to run the new request. Thread variables are not initialized when the thread resumes, which means the code cannot assume any initial value for these variables.

The “request” variable, however, stored in .NET’s HttpRequest object, always works. Because a new instance is created once and only once for each request, and because we used a field which is always initialized to 0, we can use the value as if it were global, while being certain no other requests – past, future, or concurrent – will interfere.

    Conclusions

It seems obvious that global variables should be avoided in multi-threaded applications (unless, of course, they’re absolutely necessary and all access to them is fully synchronized), but developers often forget that class variables suffer from the same limitation. Thread variables, or class variables with the [ThreadStatic] attribute can be used in multi-threaded applications, but applications cannot assume these variables have been initialized.

ASP.NET’s current request (HttpRequest.Current) can always be assumed to be created when a request starts, and discarded when the request ends. This allows the creation of thread-safe, globally accessible, initialized variables.

While using the HttpRequest object is specific to ASP.NET, the methods outlined in this article should apply to any multi-threaded middle-tier technology, including web applications, data access layers, and business logic layers.

    Source Code

    Default.aspx

<%@ Page language="c#" Debug="true" Codebehind="Default.pas"
    AutoEventWireup="false" Inherits="Default.TDefault" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head runat="server">
    <title>Shared variable types
</title>
  </head>
  <body>
     <form runat="server">
       <p>Enter a test value: <asp:TextBox id="ValueEdit" runat="server"></asp:TextBox>
          <asp:Button id="RunButton" runat="server" text="Run"></asp:Button></p>
       <table cellspacing="1" cellpadding="1">
          <tr>
            <td><asp:Label id="ClassLabel" runat="server"></asp:Label></td>
            <td><asp:Label id="ThreadLabel" runat="server"></asp:Label></td>
            <td><asp:Label id="ThreadStaticLabel" runat="server"></asp:Label></td>
            <td><asp:Label id="RequestLabel" runat="server"></asp:Label></td>
            <td><asp:Label id="GlobalLabel" runat="server"></asp:Label></td>
          </tr>
       </table>
     </form>
  </body>
</html>

    Default.pas

unit Default;

interface

uses
  System.Collections, System.ComponentModel,
  System.Data, System.Drawing, System.Web, System.Web.SessionState,
  System.Web.UI, System.Web.UI.WebControls, System.Web.UI.HtmlControls,
  System.Web.Security, System.Web.UI.WebControls.WebParts, System.Configuration;

type
  TDefault = class(System.Web.UI.Page)
  {$REGION 'Designer Managed Code'}
  strict private
    procedure InitializeComponent;
    procedure RunButton_Click(sender: System.Object; e: System.EventArgs);
  {$ENDREGION}
  strict private
    procedure Page_Load(sender: System.Object; e: System.EventArgs);
  strict protected
    ValueEdit: System.Web.UI.WebControls.TextBox;
    RunButton: System.Web.UI.WebControls.Button;
    ClassLabel: System.Web.UI.WebControls.Label;
    ThreadLabel: System.Web.UI.WebControls.Label;
    RequestLabel: System.Web.UI.WebControls.Label;
    GlobalLabel: System.Web.UI.WebControls.Label;
    ThreadStaticLabel: System.Web.UI.WebControls.Label;
  protected
    procedure OnInit(e: EventArgs); override;
  private
    function FormatResults(Title: string;
      StartValue, ChangedValue, FinalValue: Integer): string;
  public
    { Public Declarations }
  end;

  Shared = class abstract
  public
    class procedure SetValue(v: Integer); virtual; abstract;
    class function GetValue: Integer; virtual; abstract;
  end;

  SharedClassVar = class(Shared)
  private
    class var
      FValue: Integer;
  public
    class procedure SetValue(v: Integer); override;
    class function GetValue: Integer; override;
  end;

  SharedThreadVar = class(Shared)
  public
    class procedure SetValue(v: Integer); override;
    class function GetValue: Integer; override;
  end;

  SharedThreadStaticVar = class(Shared)
  private
    class var
      [ThreadStatic]
      FValue: Integer;
  public
    class procedure SetValue(v: Integer); override;
    class function GetValue: Integer; override;
  end;

  SharedRequest = class(Shared)
  private
    FValue: Integer;
  public
    class procedure SetValue(v: Integer); override;
    class function GetValue: Integer; override;
    class function Current: SharedRequest;
  end;

  SharedGlobalVar = class(Shared)
  public
    class procedure SetValue(v: Integer); override;
    class function GetValue: Integer; override;
  end;

implementation

uses
  System.Text, System.Threading;

threadvar
  SharedValue: Integer;

var
  GlobalValue: Integer = 0;

{$REGION 'Designer Managed Code'}
/// <summary>
/// Required method for Designer support -- do not modify
/// the contents of this method with the code editor.
/// </summary>
procedure TDefault.InitializeComponent;
begin
  Include(Self.RunButton.Click, Self.RunButton_Click);
  Include(Self.Load, Self.Page_Load);
end;
{$ENDREGION}

procedure TDefault.Page_Load(sender: System.Object; e: System.EventArgs);
begin
  // TODO: Put user code to initialize the page here
end;

procedure TDefault.OnInit(e: EventArgs);
begin
  //
  // Required for Designer support
  //
  InitializeComponent;
  inherited OnInit(e);
end;

procedure TDefault.RunButton_Click(sender: System.Object; e: System.EventArgs);
var
  ClassStartValue: Integer;
  ClassChangedValue: Integer;
  ClassFinalValue: Integer;
  ThreadStartValue: Integer;
  ThreadChangedValue: Integer;
  ThreadFinalValue: Integer;
  ThreadStaticStartValue: Integer;
  ThreadStaticChangedValue: Integer;
  ThreadStaticFinalValue: Integer;
  RequestStartValue: Integer;
  RequestChangedValue: Integer;
  RequestFinalValue: Integer;
  GlobalStartValue: Integer;
  GlobalChangedValue: Integer;
  GlobalFinalValue: Integer;
begin
  { Save initial values }
  ClassStartValue := SharedClassVar.GetValue;
  ThreadStartValue := SharedThreadVar.GetValue;
  ThreadStaticStartValue := SharedThreadStaticVar.GetValue;
  RequestStartValue := SharedRequest.GetValue;
  GlobalStartValue := SharedGlobalVar.GetValue;

  { Set statup values }
  SharedClassVar.SetValue(Convert.ToInt32(ValueEdit.Text));
  SharedThreadVar.SetValue(Convert.ToInt32(ValueEdit.Text));
  SharedThreadStaticVar.SetValue(Convert.ToInt32(ValueEdit.Text));
  SharedRequest.SetValue(Convert.ToInt32(ValueEdit.Text));
  SharedGlobalVar.SetValue(Convert.ToInt32(ValueEdit.Text));

  { Save changed values }
  ClassChangedValue := SharedClassVar.GetValue;
  ThreadChangedValue := SharedThreadVar.GetValue;
  ThreadStaticChangedValue := SharedThreadStaticVar.GetValue;
  RequestChangedValue := SharedRequest.GetValue;
  GlobalChangedValue := SharedGlobalVar.GetValue;

  { Sleep for 10 seconds to allow user to run another request }
  Thread.Sleep(10000);

  { Get final values }
  ClassFinalValue := SharedClassVar.GetValue;
  ThreadFinalValue := SharedThreadVar.GetValue;
  ThreadStaticFinalValue := SharedThreadStaticVar.GetValue;
  RequestFinalValue := SharedRequest.GetValue;
  GlobalFinalValue := SharedGlobalVar.GetValue;

  { Display results }
  ClassLabel.Text := FormatResults(
    'Class variable', ClassStartValue, ClassChangedValue, ClassFinalValue);
  ThreadLabel.Text := FormatResults(
    'Thread variable', ThreadStartValue, ThreadChangedValue, ThreadFinalValue);
  ThreadStaticLabel.Text := FormatResults(
    'Thread static variable', ThreadStaticStartValue,
      ThreadStaticChangedValue, ThreadStaticFinalValue);
  RequestLabel.Text := FormatResults(
    'Request variable', RequestStartValue, RequestChangedValue, RequestFinalValue);
  GlobalLabel.Text := FormatResults(
    'Global variable', GlobalStartValue, GlobalChangedValue, GlobalFinalValue);
end;

function TDefault.FormatResults(Title: string; StartValue, ChangedValue,
  FinalValue: Integer): string;
var
  Builder: StringBuilder;
begin
  Builder := StringBuilder.Create;
  Builder.Append('<p>');
  Builder.Append(Title);
  Builder.Append('</p><ul>');
  Builder.Append('<li style="color: ');
  if StartValue = 0 then
    Builder.Append('green')
  else
    Builder.Append('red');
  Builder.Append('">Start: ');
  Builder.Append(StartValue);
  Builder.Append('</li>');
  Builder.Append('<li>Changed: ');
  Builder.Append(ChangedValue);
  Builder.Append('</li>');
  Builder.Append('<li style="color: ');
  if FinalValue = ChangedValue then
    Builder.Append('green')
  else
    Builder.Append('red');
  Builder.Append('">Final: ');
  Builder.Append(FinalValue);
  Builder.Append('</li>');
  Builder.Append('</ul>');

  Result := Builder.ToString;
end;

{ SharedClassVar }

class function SharedClassVar.GetValue: Integer;
begin
  Result := FValue;
end;

class procedure SharedClassVar.SetValue(v: Integer);
begin
  FValue := v;
end;

{ SharedThreadVar }

class function SharedThreadVar.GetValue: Integer;
begin
  Result := SharedValue;
end;

class procedure SharedThreadVar.SetValue(v: Integer);
begin
  SharedValue := v;
end;

{ SharedThreadStaticVar }

class function SharedThreadStaticVar.GetValue: Integer;
begin
  Result := FValue;
end;

class procedure SharedThreadStaticVar.SetValue(v: Integer);
begin
  FValue := v;
end;

{ SharedRequest }

class function SharedRequest.Current: SharedRequest;
begin
  if Assigned(HttpContext.Current.Items['SharedRequest']) then
    Result := HttpContext.Current.Items['SharedRequest'] as SharedRequest
  else
  begin
    Result := SharedRequest.Create;
    HttpContext.Current.Items['SharedRequest'] := Result;
  end;
end;

class function SharedRequest.GetValue: Integer;
begin
  Result := Current.FValue;
end;

class procedure SharedRequest.SetValue(v: Integer);
begin
  Current.FValue := v;
end;

{ SharedGlobalVar }

class function SharedGlobalVar.GetValue: Integer;
begin
  Result := GlobalValue;
end;

class procedure SharedGlobalVar.SetValue(v: Integer);
begin
  GlobalValue := v;
end;

end.

Server Response from: ETNASC02