Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebClient async callback not called in ASP.NET MVC

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;
    }
}
like image 260
THX-1138 Avatar asked Apr 19 '11 15:04

THX-1138


1 Answers

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.

like image 162
RandomEngy Avatar answered Sep 17 '22 04:09

RandomEngy