Reputation: 11
I need to implement a service which fire a start of the processing. But I don't need to wait for result. I can just show the default output and in the background the process will be working.
However I came up to the problem that the code after await is not executed.
I prepared some code to show the idea:
public class HomeController : Controller
{
public ActionResult Deadlock()
{
AsyncCall();
return View();
}
private async Task AsyncCall()
{
await Task.Delay(10000);
var nonreachablePlace = "The breakpoint will not set the execution here";
Do(nonreachablePlace);
}
private void Do(string m)
{
m.Contains("x");
}
}
I know that it looks very bad. But the idea was like:
To my bad 6 step is not processed. I have tried to set up the breakpoint and never got hit.
But if I reduce the time delay and do it in debug I will come to the 6 step.
Enter to the controller's action method
After return from controller's action method
But if I leave only one breakpoint after await, I won't go to the 6 step
var nonreachablePlace = "The breakpoint will not set the execution here";
NOTE:
I append ConfigureAwait(false)
to the Task.Delay()
. It looks like this:
private async Task AsyncCall()
{
await Task.Delay(10000).ConfigureAwait(false);
var nonreachablePlace = "The breakpoint will not set the execution here";
Do(nonreachablePlace);
}
And now it works as expected.
My question is why does the code not work without ConfigureAwait(false)?
Maybe it somehow related to SynchronizationContext
and it is not reachable after the main thread finishes its work. And after that awaitable method tryes to get a context when it has been already disposed (Just my thought)
Upvotes: 1
Views: 1348
Reputation: 11
I dived deep into the logic behind Task.Delay(10000)
and continuation code after that.
Thanks to the post made by Stephen Toub.
The main problem was in part when a Task had finished. And it's result needed to be processed by next thread.
Since I hadn't written ConfigureAwait()
I implicitly meant to run the code in a thread which has SynchronizationContext
(AspNetSynchronizationContext
in my case).
private async Task AsyncCall()
{
/// The delay is done by a thread from a ThreadPool.
await Task.Delay(10000);
/// After the Task has been finished
/// TaskAwaiter tryies to send a continuation code to a thread with
/// SynchronizationContext.
var nonreachablePlace = "The breakpoint will not set the execution here";
Do(nonreachablePlace);
}
Because I hadn't wanted to wait the result of awaitable, I returned the response from controller's action. Then the thread went to ThreadPool and SynchronizationContext was disposed. To the moment of Task completion, there was no SynchronizationContext to send a delegate with continuation code.
And during the code creation Visual Studio Enabled Just My Code
option was set to true. That's why this exception was thrown silently to me.
And about the situation when I was able to run a code even I had Task.Delay(2000)
. I think it's caused by the time needed to Classic ASP.NET to complete a request and create a response to it. During this time you can get a reference to SynchronizationContext
and Post
a delegate to it.
Upvotes: 0
Reputation: 1
So Deadlock()
calls AsyncCall()
Then AsyncCall()
Tells DeadLock()
"Okay, well I'm waiting for Task.Delay
to count to 10,000 but you can go ahead."
...so AsyncCall()
yields the main thread back over to DeadLock()
.
Now DeadLock()
never said anything about waiting on AsyncCall()
to finish, so as far as DeadLock()
is concerned, AsyncCall()
has already returned (it actually only yielded, but the program cursor would still be passed back out into DeadLock()
.
So I would suggest setting your breakpoint at the AsyncCall()
method in DeadLock()
, because you'll probably see that your main thread is already done and exited before the Task.Delay()
is even done.
So AsyncCall()
never even gets a chance to finish Awaiting
.
Upvotes: 0
Reputation: 155005
Use HostingEnvironment.QueueBackgroundWorkItem
.
Note this is only available in Classic ASP.NET on .NET Framework (System.Web.dll
) and not ASP.NET Core (I forget to what extent it works in ASP.NET Core 1.x and 2.x running on .NET Framework, but anyway).
All you need is this:
using System.Web.Hosting;
public class MyController : Controller
{
[HttpPost( "/foo" )]
public async Task<ActionResult> DoSomething()
{
HostingEnvironment.QueueBackgroundWorkItem( this.DoSomethingExpensiveAsync ); // Pass the method by name or as a `Func<CancellationToken,Task>` delegate.
return this.View();
}
private async Task DoSomethingExpensiveAsync( CancellationToken cancellationToken )
{
await Task.Delay( TimeSpan.FromSeconds( 30 ) );
}
}
You can also use it with non-async workloads:
[HttpPost( "/foo" )]
public async Task<ActionResult> DoSomething()
{
HostingEnvironment.QueueBackgroundWorkItem( this.DoSomethingExpensive ); // Pass the method by name or as a `Action<CancellationToken>` delegate.
return this.View();
}
private void DoSomethingExpensive( CancellationToken cancellationToken )
{
Thread.Sleep( 30 * 1000 ); // NEVER EVER EVER call Thread.Sleep in ASP.NET!!! This is just an example!
}
If you want to start a job normally at first and only finish it in the background if it takes too long, then do this:
[HttpPost( "/foo" )]
public async Task<ActionResult> DoSomething()
{
Task<String> workTask = this.DoSomethingExpensiveAsync( default );
Task timeoutTask = Task.Delay( TimeSpan.FromSeconds( 5 ) );
Task first = await Task.WhenAny( workTask, timeoutTask );
if( first == timeoutTask )
{
// `workTask` is still running, so resume it in the background:
HostingEnvironment.QueueBackgroundWorkItem( async ct => await workTask );
return this.View( "Still working..." );
}
else
{
// `workTask` finished before the timeout:
String result = await workTask; // or just `workTask.Result`.
return this.View( result );
}
}
private async Task<String> DoSomethingExpensiveAsync( CancellationToken cancellationToken )
{
await Task.Delay( TimeSpan.FromSeconds( 30 ) );
return "Explosive bolts, ten thousand volts; At a million miles an hour, Abrasive wheels and molten metals";
}
Upvotes: 2