【问题标题】:Fire and forget async Task vs Task.Run触发并忘记异步任务与 Task.Run
【发布时间】:2016-06-21 19:45:11
【问题描述】:

我在开发一些 ASP.Net 应用程序时看到一些空引用问题。例外情况如下:

Description: The process was terminated due to an unhandled exception.
Exception Info: System.NullReferenceException
Stack:
   at System.Threading.Tasks.AwaitTaskContinuation.<ThrowAsyncIfNecessary>b__1(System.Object)
   at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
   at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
   at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

通过谷歌和SO搜索后,很可能是我的一些代码以这种方式触发并忘记了异步任务:

public interface IRepeatedTaskRunner
{
    Task Start();
    void Pause();
}

public class RepeatedTaskRunner : IRepeatedTaskRunner
{
    public async Task Start() //this is returning Task
    {
        ...
    }
}

public class RepeatedTaskRunnerManager{
    private IRepeatedTaskRunner _runner;
    public void Foo(){
        _runner.Start(); //this is not awaited.
    }
}

我认为这与这个问题"Fire and forget async method in asp.net mvc" 非常相似,但并不完全相同,因为我的代码不在控制器上并接受请求。我的代码专门用于运行后台任务。

与上面的 SO 问题一样,通过将代码更改为下面的代码来解决问题,但我只想知道为什么以及从 Task.Run 返回的任务和触发和忘记异步任务有什么区别:

public interface IRepeatedTaskRunner
{
    Task Start();
    void Pause();
}

public class RepeatedTaskRunner : IRepeatedTaskRunner
{
    public Task Start() // async is removed.
    {
        ...
    }
}

public class RepeatedTaskRunnerManager{
    private IRepeatedTaskRunner _runner;
    public void Foo(){
        Task.Run(() => _runner.Start()); //this is not waited.
    }
}

从我的角度来看,这两个代码都只是创建了一个任务而忘记了它。唯一的区别是第一个任务没有等待。这会导致行为上的差异吗?

从另一个 SO 问题中,我知道火灾和遗忘模式的局限性和不良影响。

标签: c# asp.net multithreading asynchronous async-await


【解决方案1】:

我只是想知道为什么和从 Task.Run 返回的任务与触发和忘记异步任务有什么区别

区别在于AspNetSynchronizationContext是否被捕获,即“请求上下文”(包括当前文化、会话状态等)。

直接调用时,Start 在该请求上下文中运行,await 默认情况下将捕获该上下文并在该上下文中恢复(更多信息on my blog)。这可能会导致“有趣”的行为,例如,如果 Start 尝试在请求上下文中恢复已完成的请求。

使用Task.Run 时,Start 在没有请求上下文的不同线程池线程上运行。因此,await 不会捕获上下文,而是会在任何可用的线程池线程上恢复。

我知道您已经知道这一点,但我必须向 Google 员工重申:请记住,以这种方式排队的任何工作都不可靠。至少,你应该使用HostingEnvironment.QueueBackgroundWorkItem(或者我的AspNetBackgroundTasks library,如果你还没有使用4.5.2)。这些都非常相似;他们会将后台工作排队到线程池(实际上是我的库的BackgroundTaskManager.Run does call Task.Run),但是这两种解决方案也将采取额外的步骤注册 与 ASP.NET 运行时一起工作。这还不足以使后台工作真正意义上的“可靠”,但它确实最大限度地减少您丢失工作的机会。

【讨论】:

  • 您博客上的优秀文章!我认为在开始使用这些等待之前,我确实需要阅读更多类似的教程,尤其是对于捕获的上下文部分!它也可能解释了为什么当我将这些代码放在单元测试中时,它没有抛出。只是因为捕获上下文(我认为它只是主线程)仍然存在,对吧?
  • @bigbearzhu:取决于您的测试框架。如果您使用的是 MSTest,那么首先没有要捕获的上下文。 xUnit 提供自己的上下文,但在单元测试完成之前不会被释放。
  • 确实! SynchronizationContext.Current 在创建任务之前为空(我正在使用 MSTest)。非常感谢。
  • @StephenCleary 将在 web.config 文件中添加应用程序键 "aspnet:UseTaskFriendlySynchronizationContext" = "true" 通过首先设置正确的请求上下文?
  • @Brain2000:您必须使用UseTaskFriendlySynchronizationContext(或将targetFramework 设置为4.5 或更高),否则await 的行为完全未定义。它不会解决 await 捕获然后完成的上下文的情况 - 在这种情况下,适当的解决方案是修复损坏的代码。
猜你喜欢
  • 2014-12-06
  • 2014-05-05
  • 1970-01-01
  • 1970-01-01
  • 2020-04-25
  • 2020-07-01
  • 2010-09-14
  • 1970-01-01
  • 2013-09-01
相关资源
最近更新 更多