views:

376

answers:

5

I think I need some help understanding how static objects persist in an ASP.Net application. I have this scenario:

someFile.cs in a class library:

public delegate void CustomFunction();

public static class A {
    public static CustomFunction Func = null;
}

someOtherFile.cs in a class library:

public class Q {
    public Q() {
        if (A.Func != null) {
            A.Func();
        }
    }
}

Some ASP.Net page:

Page_Init {
    A.Func = MyFunc;
}

public void MyFunc() {
    System.IO.File.AppendAllText(
        "mydebug.txt", DateTime.Now.ToString("hh/mm/ss.fff", Session.SessionID));
}

Page_Load {
    Q myQ = new Q();
    System.Threading.Thread.Sleep(20000);
    mQ = new Q();
}

The idea is that I have a business object which does some operation based on a callback function at the UI level. I set the callback function to a static variable on Page_Init (in the real code version, in the Master page, if that makes a difference). I thought that every execution of the page, no matter what user session it came from, would go through that function's logic but operate on its own set of data. What seems to be happening instead is a concurrency issue.

If I run one user session, then while it is sleeping between calls to that callback function, start another user session, when the first session comes back from sleeping it picks up the session ID from the second user session. How can this be possible?

Output of mydebug.txt:

01/01/01.000 abababababab  (session #1, first call)
01/01/05.000 cdcdcdcdcdcd  (session #2, first call - started 5 seconds after session #1)
01/01/21.000 cdcdcdcdcdcd  (session #1 returns after the wait but has assumed the function context from session #2!!!!!)
01/01/25.000 cdcdcdcdcdcd  (session #2 returns with its own context)

Why is the function's context (meaning, its local data, etc.) being overwritten from one user session to another?

+9  A: 

Each request to an asp.net site comes in and is processed on it's own thread. But each of those threads belong to the same application. That means anything you mark as static is shared across all requests, and therefore also all sessions and users.

In this case, the MyFunc function that's part of your page class is copied over top of the static Func member in A with every page_init, and so every time any user does a page_init, he's replacing the A.Func used by all requests.

Joel Coehoorn
I do understand that the function address is being replaced. I don't understand, however, why the data being accessed IN the function is also crossing over. Why does session #1, which has Session.SessionID = abababababab, when calling myFunc again, suddenly have Session.SessionID = cdcdcdcdcdcd?
Matt Hamsmith
Because the delegate captures the entire environment of that method, including the current session object when you assign it to the static delegate.
nos
Because before Session#1 calls the static A.Func() method in the Q() constructor session Session#2 replaced that function with it's own implementation. You're calling a completely different function the 2nd time round.
Joel Coehoorn
It does this by means of a closure.
Joel Coehoorn
+1 Once Jeff mentioned a closure, the light clicked for me. Thank you for your answer and comments.
Matt Hamsmith
+4  A: 

Static data is shared among the entire application domain of your webapp. In short, it's shared among all the threads serving requests in your webapp, it's not bound to a session/thread/user in any way but to the webapp as a whole.(unlike e.g. php where each request lives in its own isolated environment bar a few knobs provided - such as the session variable.)

nos
Like I'm trying to verbalize properly in my comment to Joel, I understand why the function reference is shared among the app domain. I don't understand why the variables that ARE session/thread/user bound within the function's logic are also being shared among the application domain.
Matt Hamsmith
+2  A: 

One solution you might consider is using [ThreadStatic].

http://msdn.microsoft.com/en-us/library/system.threadstaticattribute%28VS.71%29.aspx

It will make your statics per thread. There are cavaets however so you should test.

Nissan Fan
Besides the MSDN note in your link about initial values, what are the caveats?
Matt Hamsmith
This won't work in ASP.NET. You can't predict which thread will service a given request.
Brannon
@Brannon - but are we not guaranteed that a single thread will service a single request? I think that is my issue - that one thread's myFunc got changed in the middle of the operation by another thread's assignment of myFunc.
Matt Hamsmith
An alternative to ThreadStatic variables is HttpContext.Current.Items which is scoped to a distinct request (see http://www.hanselman.com/blog/ATaleOfTwoTechniquesTheThreadStaticAttributeAndSystemWebHttpContextCurrentItems.aspx for instance).
sighohwell
@Matt - in general, a single thread will service the request. The problem with ThreadStatic, is that the values will persist on that thread after the request ends. While this may work in your scenario, it is not what you want. Use HttpContext.Items.
Brannon
@Nissan +1 - testing so far with ThreadStatic seems to fix my problem. I'm concerned about the caveats and what Brannon raised, particularly since the production version of this code will have potentially dozens of concurrent users.
Matt Hamsmith
@Brannon +1 - I think that is still fine in my scenario. As long as the same thread services one request's entire lifecycle, I believe I am safe. The first thing that happens on each request is a reassignment of that static variable, giving it the "scope" of the new request.
Matt Hamsmith
Just remember: the same user will likely end up on many different threads over the course of a session.
Joel Coehoorn
@Matt - You should really look at using HttpContext.Items, it is /designed/ for this exact scenario!
Brannon
+2  A: 

I won't try to improve on the other answers' explanations of static members, but do want to point out another way to code around your immediate problem.

As a solution, you could make an instance-oriented version of your class A, store it in a page-level variable, and pass it to Q's constructor on page load:

public class MyPage: Page {
    private A2 _a2;

    // I've modified A2's constructor here to accept the function
    protected Page_Init() { this._a2 = new A2(MyFunc); }

    protected Page_Load() { 
        Q myQ = new Q(this._a2); 
        // etc..
    }
}

In fact, if there's no pressing need to declare A2 earlier, you could just instantiate it when you create your instance of Q in Page_Load.

Edit: to answer the question you raised in other comments, the reason the variables are being shared is that the requests are sharing the same delegate, which has only a single copy of its variables. See Jon Skeet's The Beauty of Closures for more details.

Jeff Sternal
DOH!!!!!!! It makes entire sense to me now. I didn't even realize that I had set up a closure in this case. My example code is an extremely simplified version of the real code - your proposed solution won't fit the real case, unfortunately. But thanks much for the closure explanation.
Matt Hamsmith
+1  A: 

If you want the data to persist only for the current request, use HttpContext.Items: http://msdn.microsoft.com/en-us/library/system.web.httpcontext.items.aspx

If you want the data to persist for the current user's session (assuming you have session state enabled), use HttpContext.Session: http://msdn.microsoft.com/en-us/library/system.web.httpcontext.session.aspx

Brannon