Fire and forget异步任务 vs Task.Run

3

在开发一些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()

在通过Google和SO进行搜索后,很可能是由于我的代码以fire and forget异步任务的方式产生了问题。
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问题相同,通过将代码更改为以下内容可以解决问题,但我想知道为什么以及使用fire-and-forgetting async Task和从Task.Run返回的Task之间有什么区别:
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.
    }
}

从我的角度来看,这两个代码只是创建一个任务并忘记它。唯一的区别在于第一个没有在任务上使用await。这会导致行为上的差异吗?
我意识到fire and forget模式的限制和不良影响,来自其他SO问题的了解。

1
在帖子中没有显示导致问题的代码...但是您的Foo方法的调用者没有提供正确的同步上下文(使用默认的ASP.Net上下文),这会尝试返回到当前请求的线程。 - Alexei Levenkov
@AlexeiLevenkov,你说的“正确的同步上下文(通过使用默认的ASP.Net one)”,是什么意思?你能给我提供一些例子吗?谢谢。 - bigbearzhu
@AlexBell 谢谢,正如我在问题中所述,我知道 Task.Run 是更好的选择,但我不明白为什么在这里使用 async Task 会导致空引用异常。如果有人能给出更清晰的解释就好了。 - bigbearzhu
2
首先,await会捕获当前的同步上下文(请参见https://dev59.com/eHLYa4cB1Zd3GeqPa7ZV以尝试避免这种情况 - 即Task.Run是一个好选择)。对于不能返回到请求线程的方法,您需要强制使用不在执行完成时回调请求线程的上下文(因为该线程可能正在运行其他请求或已经结束)。还可以查看http://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx(以及第1/2部分)。 - Alexei Levenkov
@AlexeiLevenkov,感谢您的努力回答我的问题,这帮助我理解了我的问题背后的原因,并阅读了Stephen Cleary的博客文章:http://blog.stephencleary.com/2012/02/async-and-await.html。 - bigbearzhu
显示剩余2条评论
1个回答

17
区别在于是否捕获了AspNetSynchronizationContext,它是“请求上下文”(包括当前文化、会话状态等)。
直接调用时,Start在该请求上下文中运行,并且默认情况下await会捕获该上下文并在该上下文中恢复执行(更多信息请参见我博客)。如果Start尝试在已经完成的请求上下文中恢复,则可能导致“有趣”的行为。
使用Task.Run时,Start在不具有请求上下文的不同线程池线程上运行。因此,await不会捕获任何上下文,并将在任何可用的线程池线程上恢复执行。

我知道你已经知道这个问题,但是我必须为谷歌的用户重申一下:请记住,通过此方式排队的任何工作都不可靠。至少,你应该使用HostingEnvironment.QueueBackgroundWorkItem(如果你还没有升级到4.5.2版本,则可以使用我的AspNetBackgroundTasks库)。这两者非常相似;它们都将后台工作排队到线程池中(实际上,我的库的BackgroundTaskManager.Run确实调用了Task.Run),但这两种解决方案还将采取额外的步骤,与ASP.NET运行时一起注册该工作。这并不足以使后台工作在真正意义上变得“可靠”,但它确实最小化了你丢失工作的几率。


你的博客上有一篇很棒的文章!在开始使用那些等待操作之前,我认为我需要阅读更多类似的教程,特别是关于捕获上下文部分的内容!这也可能解释了为什么当我将这些代码放入单元测试中时,它没有抛出异常。这只是因为捕获的上下文(我认为是主线程)仍然存在,对吗? - bigbearzhu
@bigbearzhu:这取决于你的测试框架。如果你使用的是MSTest,那么首先没有上下文需要捕获。xUnit提供了自己的上下文,但它直到单元测试完成后才会被处理。 - Stephen Cleary
确实!在创建任务之前,SynchronizationContext.Current为null(我正在使用MSTest)。非常感谢。 - bigbearzhu
@StephenCleary 在 web.config 文件中添加应用程序密钥 "aspnet:UseTaskFriendlySynchronizationContext" = "true" 是否可以解决所描述的“有趣”行为,即通过首先设置正确的请求上下文来恢复线程完成请求? - Brain2000
当不使用IIS与ASP.NET时,“googlers”的最后一段是否适用?我理解问题出现在进程被IIS关闭时。 - mcont
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接