MVC4 + async/await + return response before action

2019-02-01 11:00发布

问题:

In my MVC4 app I need to add a controller for uploading and processing large files. Immediately after the file is uploaded I need to start async processing of that file and return response to the browser without waiting for the processing to complete.

Obviously I could start a new thread for processing the file manually, but I'm wondering if I can implement this scenario using async/await mechanism introduced with .net 4.5

To test the concept I've tried something like this:

public async Task<ActionResult> Test()
{
    TestAsync();
    return View("Test");
}

public async void TestAsync()
{
    await LongRunning();
}

private Task<int> LongRunning()
{
    return Task<int>.Factory.StartNew(() => Pause());
}

private int Pause()
{
    Thread.Sleep(10000);
    return 3;
}

The async mechanism seems to work in general: when I debug the code I hit the "return View("Test");" line before the line "return 3". However, the browser receives the response only after the Pause method completes.

This seems to behave like regular async controllers (the ones with Async and Completed methods). Is there a way to use async/await in controllers for my scenario?

回答1:

Obviously I could start a new thread for processing the file manually, but I'm wondering if I can implement this scenario using async/await mechanism introduced with .net 4.5

No, you cannot, because async doesn't change the HTTP protocol.

Svick and James have already posted the correct answers as comments, which I duplicate below for convenience:

IIS can recycle your application pretty much at any time.

If you have long-running things to do async to the request, do them elsewhere. The 'typical' is a persistent queue (MSMQ, Azure, RabbitMQ, etc) with something else (windows service, exe run by task scheduler, app using Quartz.net, etc) processing them.

To summarize, HTTP gives you one request and one response (async - and anything else - won't change this).

ASP.NET is designed around HTTP requests (and makes assumptions like "if there are no outstanding requests, then it's safe to stop this web site"). You can kick off a new thread and keep your upload in memory (which is the easiest way to do it), but it's strongly not recommended.

For your situation, I recommend you follow James' suggestion:

  • When your upload completes, save the upload to persistent storage (e.g., an Azure queue), and return a "ticket" to the browser.
  • Have some other process (e.g., an Azure worker role) process the queue, eventually marking it as complete.
  • Have the browser poll the server with its "ticket". The server uses the "ticket" to look up the file in persistent storage and return whether or not it's complete.

There are some variations to this (e.g., using SignalR to notify the browser when processing is complete), but the general architecture is the same.

This is complex, but it's the right way to do it.



回答2:

You have an error in your code. You must await the call to TestAsync in your action. If you don't, it simply returns a "Task" object without running anything.

public async Task<ActionResult> Test()
{
    await TestAsync();
    return View("Test");
}

and the correct way to sleep in an async method is to call

await Task.Delay(10000);

You have to change the signature of TestAsync: you should not mark a method async if it returns void. It must return Task instead. Returning void is reserved for compatibility with .net events.

public async Task TestAsync()
{
    await LongRunning();
}

The method body is the same.



回答3:

Your LongRunning method is synchronously sleeping 10 seconds. Change it so the sleep happens in the task instead.