开发者

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 wit开发者_运维技巧hout 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;
    }
}


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.


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.


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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜