从同步操作方法中调用带有 await 的异步方法

问题描述

我需要实现一个启动处理的服务。但我不需要等待结果。我可以只显示输出,并在后台运行该进程。
但是我遇到了await之后的代码没有执行的问题。

我准备了一些代码来展示这个想法:

//GEN-BEGIN  //GEN-END

我知道它看起来很糟糕。但这个想法是这样的:

  1. 一个线程转到死锁方法
  2. 线程同步转到 AsyncCall 方法
  3. 面对 await 语句。
  4. 从死锁方法开始。
  5. 继续 main 方法到最后。
  6. Task.Delay 完成后,该线程将从线程池中出现并继续工作。

我的坏 6 步未处理。我尝试设置断点但从未被击中。

但如果我减少时间延迟并在调试中进行,我将进入第 6 步。
Enter to the controller's action method

After return from controller's action method

但是如果我在await后只留下一个断点,我就不会走到第6步
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"); } }

注意:
我将 var nonreachablePlace = "The breakpoint will not set the execution here"; 附加到 ConfigureAwait(false)。它看起来像这样:

Task.Delay()

现在它按预期工作了。

我的问题是为什么没有 ConfigureAwait(false) 的代码不能工作?
也许它与 private async Task AsyncCall() { await Task.Delay(10000).ConfigureAwait(false); var nonreachablePlace = "The breakpoint will not set the execution here"; Do(nonreachablePlace); } 有某种关系,并且在主线程完成其工作后无法访问。然后等待方法尝试在已经处理的情况下获取上下文(只是我的想法)

解决方法

使用HostingEnvironment.QueueBackgroundWorkItem

请注意,这仅在 .NET Framework (System.Web.dll) 上的 Classic ASP.NET 中可用,而在 ASP.NET Core 中不可用(我忘记了它在 ASP.NET Core 中的工作范围1.x 和 2.x 在 .NET Framework 上运行,但无论如何。

你只需要这个:

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 ) );
    }
}

您还可以将其用于非异步工作负载:

    [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!
    }

如果你想一开始正常开始工作,如果时间太长只在后台完成,那么这样做:

    [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";
    }
,

所以 Deadlock() 调用 AsyncCall()

然后AsyncCall()告诉DeadLock()“好吧,我在等Task.Delay数到10,000,但你可以继续。” ...所以 AsyncCall() 将主线程返回到 DeadLock()

现在DeadLock()从来没有说过要等待AsyncCall()完成,所以就DeadLock()而言,AsyncCall()已经返回了(实际上只是屈服了,但是程序游标仍会被传回 DeadLock()

所以我建议在 AsyncCall() 中的 DeadLock() 方法中设置断点 ,因为您可能会看到您的主线程已经完成并在 { {1}} 甚至已经完成。

所以Task.Delay()甚至都没有机会完成AsyncCall()

,

我深入研究了 Task.Delay(10000) 背后的逻辑以及之后的继续代码。
感谢 Stephen Toub 制作的 post
主要问题部分在于任务完成时。并且它的结果需要由下一个线程处理。
由于我没有编写 ConfigureAwait(),因此我隐含地打算在具有 SynchronizationContext(在我的情况下为 AspNetSynchronizationContext)的线程中运行代码。

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);
}

因为不想等待 awaitable 的结果,所以返回了控制器 action 的响应。然后线程转到 ThreadPool 并处理 SynchronizationContext。 到任务完成的那一刻,没有 SynchronizationContext 发送带有继续代码的委托。 No SinchronizationContext

在代码创建过程中,Visual Studio Enabled Just My Code 选项设置为 true。这就是为什么这个异常被默默地抛出给我。

还有关于即使我有 Task.Delay(2000) 也能运行代码的情况。我认为这是由 Classic ASP.NET 完成请求并创建对它的响应所需的时间造成的。在此期间,您可以获得对 SynchronizationContext 的引用和对它的 Post 委托。