5

On GET request I run (something like):

public ActionResult Index(void) {
    webClient.DownloadStringComplete += onComplete;
    webClient.DownloadStringAsync(...);
    return null;
}

I see that onComplete isn't get invoked until after Index() has finished execution. I can see that onComplete is invoked on a different thread from one Index was executed on.

Question: why is this happening? why is webClient's async thread is apparently blocked until request handling thread is finished?

Is there a way to fix this without starting new thread from ThreadPool (I tried this, and using thread pool does work as expected. Also webClient's callback does happen as expected if DownloadStringAsync is called from a ThreadPool's thread).

ASP.NET MVC 3.0, .NET 4.0, MS Cassini dev web server (VS 2010)

EDIT: Here is a full code:

public class HomeController : Controller {
    private static ManualResetEvent done;

    public ActionResult Index() {
        return Content(DownloadString() ? "success" : "failure");
    }

    private static bool DownloadString() {
        try {
            done = new ManualResetEvent(false);
            var wc = new WebClient();
            wc.DownloadStringCompleted += (sender, args) => { 
                // this breakpoint is not hit until after Index() returns.
                // It is weird though, because response isn't returned to the client (browser) until this callback finishes.
                // Note: This thread is different from one Index() was running on.
                done.Set(); 
            };

            var uri = new Uri(@"http://us.battle.net/wow/en/character/blackrock/hunt/simple");

            wc.DownloadStringAsync(uri);

            var timedout = !done.WaitOne(3000);
            if (timedout) {
                wc.CancelAsync();
                // if this would be .WaitOne() instead then deadlock occurs.
                var timedout2 = !done.WaitOne(3000); 
                Console.WriteLine(timedout2);
                return !timedout2;
            }
            return true;
        }
        catch (Exception ex) {
            Console.WriteLine(ex.Message);
        }
        return false;
    }
}
THX-1138
  • 21,316
  • 26
  • 96
  • 160

3 Answers3

5

I was curious about this so I asked on the Microsoft internal ASP.NET discussion alias, and got this response from Levi Broderick:

ASP.NET internally uses the SynchronizationContext for synchronization, and only one thread at a time is ever allowed to have control of that lock. In your particular example, the thread running HomeController::DownloadString holds the lock, but it’s waiting for the ManualResetEvent to be fired. The ManualResetEvent won’t be fired until the DownloadStringCompleted method runs, but that method runs on a different thread that can’t ever take the synchronization lock because the first thread still holds it. You’re now deadlocked.

I’m surprised that this ever worked in MVC 2, but if it did it was only by happy accident. This was never supported.

RandomEngy
  • 14,931
  • 5
  • 70
  • 113
1

This is the point of using asynchronous processing. Your main thread starts the call, then goes on to do other useful things. When the call is complete, it picks a thread from the IO completion thread pool and calls your registered callback method on it (in this case your onComplete method). That way you don't need to have an expensive thread waiting around for a long-running web call to complete.

Anyway, the methods you're using follow the Event-based Asynchronous Pattern. You can read more about it here: http://msdn.microsoft.com/en-us/library/wewwczdw.aspx

(edit) Note: Disregard this answer as it does not help answer the clarified question. Leaving it up for the discussion that happened under it.

RandomEngy
  • 14,931
  • 5
  • 70
  • 113
  • that explains that. So, WebClient doesn't use ThreadPool? How does WebClient queues up a request? I waded it with reflector for awhile but couldn't find where it happens. – THX-1138 Apr 19 '11 at 15:51
  • It's not queuing a request. It actually starts the request right then. But after starting the request DownloadStringAsync() will return and let you do other things while the download happens. During the download, there is actually no thread present at all! It only pulls a thread out of the threadpool to notify you of completion. – RandomEngy Apr 19 '11 at 15:54
  • I see. So where in code is callback invoked? I tried putting Thread.Sleep(10000) before returning from Index, but callback is still not called until after Index returns. Meaning that callback call gets queued up after Index call. I am curious how that happens. If callback is called on CLR ThreadPool it should not be blocked by GET request processing thread, is it? – THX-1138 Apr 19 '11 at 15:59
  • I see what you're saying now... but I can't repro that behavior. If I call Thread.Sleep immediately after calling DownloadStringAsync, that doesn't prevent the callback from happening. Maybe something is preventing you from seeing the effects of your callback. Are you putting the Sleep command on your UI thread? – RandomEngy Apr 19 '11 at 16:10
  • actually what I do is in the Index I wait for `done.WaitOne()` and `Complete` handler has `done.Set()`. But index gets dead-locked. So apparently `Complete` callback _actually_ doesn't get fired. Index() is called by ASP.NET (ASP.NET MVC 3.0), does it do it on UI thread? – THX-1138 Apr 19 '11 at 16:15
  • That sounds like it shouldn't deadlock. But could you post the sample and include the wait calls? There might be a bug in that logic which causes the initial thread to miss the signal somehow. Also have you put a breakpoint on the first line of onComplete to confirm that it's getting called or not? – RandomEngy Apr 19 '11 at 16:27
  • I have added code. Breakpoint in onComplete _is_ called, but only AFTER Index() has returned. – THX-1138 Apr 19 '11 at 16:59
  • I just ran your code. I put a breakpoint on the line after your first WaitOne() statement and one in your DownloadStringCompleted event handler. The event handler executed first, then after pressing F5 the line after the WaitOne() statement executed, as expected. Does this only occur when the URL you're requesting times out? Have you tried with a known good URL? – RandomEngy Apr 19 '11 at 18:49
  • Are you running it with ASP.NET MVC 3.0, .NET 4, VS 2010, in dev server (MS Cassini)? Yes, I have tried http://google.com as well – THX-1138 Apr 19 '11 at 19:06
  • I had my sample MVC project on MVC2. On MVC3 I do get your problem! That is odd indeed. Right now I don't know what the problem might be but you may be able to work around it by using an AsyncController. – RandomEngy Apr 19 '11 at 19:34
  • I worked it around by running whole DownloadString with ThreadPool, so I have it working, but it bothers me. It feels as if Cassini is configured with max request thread set to 1. I will try it with IIS Express, see if that behaves any differently. – THX-1138 Apr 19 '11 at 20:09
  • with default settings IISExpress behaves exactly the same way. I wonder if changing request or thread limits in IISExpress config files can fix this issue. – THX-1138 Apr 19 '11 at 20:18
  • Just tested with IIS proper and it's displaying the same behavior. I don't think changing the thread limits would help... IIS by default should have plenty of I/O completion threads; it wouldn't run out after 1 request. – RandomEngy Apr 19 '11 at 20:33
0

In addition to the chosen answer, see this article for further details on why the WebClient captures the SynchronizationContext.

http://msdn.microsoft.com/en-gb/magazine/gg598924.aspx

Andrew
  • 2,605
  • 3
  • 23
  • 34