AlexTurchin
AlexTurchin

Reputation: 11

Call async method with await in it from synchronous action method

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:

  1. A thread go to Deadlock method.
  2. The thread go to AsyncCall method synchronously.
  3. Faces the await statement.
  4. Go from the Deadlock method.
  5. Continue main method to the end.
  6. When Task.Delay finished, this thread will come up from the thread pool and continue working.

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

Answers (3)

AlexTurchin
AlexTurchin

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. No SinchronizationContext

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

Safyrical
Safyrical

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

Dai
Dai

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

Related Questions