在异步任务完成前返回

10
我正在开发一个ASP.NET MVC 4 Web应用程序。我使用的是.NET 4.5,并尝试利用新的异步API。我有一些情况需要在返回重要值的同时安排异步任务稍后运行。例如,这里有一个“登录”方法,我希望尽快返回一个新的SessionID,但是一旦我返回了SessionID,我想清除旧的过期SessionID。
public async Task<Guid> LogIn(string UserName, string Password)
{
    //Asynchronously get ClientID from DB using UserName and Password

    Session NewSession = new Session()
    {
        ClientID = ClientID,
        TimeStamp = DateTime.Now
    };
    DB.Sessions.Add(NewSession);
    await DB.SaveChangesAsync();    //NewSession.ID is autopopulated by DB

    CleanSessions(ClientID);    //Async method which I want to execute later

    return NewSession.ID;
}

private async void CleanSessions(int ClientID)
{
    //Asynchronously get expired sessions from DB based on ClientID and mark them for removal
    await DB.SaveChangesAsync();
}

我尝试了很多不同的方法,包括 Task.Run() 和 Parallel.Invoke() 的组合,但 CleanSessions 从未被调用。我该如何实现后台任务调度?


基于此,当SaveChangesAsync完成时,应该启动CleanSessions。它可能需要一个Taskawait来捕获错误等 - 但从我所看到的来看,它应该可以按原样工作。 - Marc Gravell
注意:除了顶级事件处理程序之外,您永远不应该在方法中使用“async void”。 - Daniel Mann
@MarcGravell:ASP.NET 会等待所有 async void 方法完成后再发送响应。但是,操作需要一种“fire-and-forget”的方式。 - Stephen Cleary
@StephenCleary 哦,我错过了 ASP.NET - 该死的同步上下文。 - Marc Gravell
2个回答

18
在ASP.NET中没有请求的情况下运行任务是不推荐的。这很危险。 话虽如此,将CleanSessions更改为返回Task,您可以像这样做:
Task.Run(() => CleanSessions());

在您的情况下,我认为这是可以的,因为如果在执行过程中未执行或被终止(由于回收而在ASP.NET中可能会发生),则没有长期问题。如果您想要通知ASP.NET有一些与请求不相关的正在进行的工作,则可以像这样使用我的博客中的BackgroundTaskManager:
BackgroundTaskManager.Run(() => CleanSessions());

1
这正是我一直在寻找的,谢谢。实际上我已经尝试过了,但我的CleanSessions()方法中有一个bug导致了异常,而且由于该方法是在没有请求的情况下执行的,所以异常没有被捕获。这让我觉得这个方法根本没有被调用!因此,对于任何想要使用这个技巧的人,请确保阅读本答案中的链接,并确保您真正理解为什么这是“危险”的。 - user1084447
四年后怎么样?或许需要更新一下了吧?你的博客自2014年以来就没有更新过,有什么新的见解吗? - CularBytes
@CularBytes:不会的,ASP.NET仍然以相同的方式工作,这个答案仍然是完全有效的。BackgroundTaskManager在ASP.NET Core上不起作用,但您可以使用IHostedService或使用IApplicationLifetime代替IRegisteredObject来完成相同的事情。 - Stephen Cleary
2
感谢更新。最终将其移动到 WebJobs,调用 API 端点并稍后执行工作(计划)。 - CularBytes

0

如果你正在尝试在 ASP.NET 中执行一些“fire and forget”的任务,那么你可能需要查看 ConfigureAwait;你可以将其作为 await 的一部分使用,告诉它忽略同步上下文。同步上下文将其与 ASP.NET 执行管道绑定在一起,但如果不必要,你可以避免它。例如:

public async Task<Guid> LogIn(string UserName, string Password)
{
    //Asynchronously get ClientID from DB using UserName and Password

    Session NewSession = new Session()
    {
        ClientID = ClientID,
        TimeStamp = DateTime.Now
    };
    DB.Sessions.Add(NewSession);
    await DB.SaveChangesAsync().ConfigureAwait(false);    //NewSession.ID is autopopulated by DB

    await CleanSessions(ClientID).ConfigureAwait(false);    //Async method which I want to execute later

    return NewSession.ID;
}
// simplified: no need for this to be full "async"
private Task CleanSessions(int ClientID)
{
    //Asynchronously get expired sessions from DB based on ClientID
    return DB.SaveChangesAsync();
}

1
这个解决方案具有最佳的错误处理(使用awaitCleanSessions上),但不会“提前”返回响应;响应将在CleanSessions完成后发送。 - Stephen Cleary
1
啊,是的 - 即使 这段代码 已经使用了 ConfigureAwait,但是响应显然不能在那些 "awaits" 完成之前发送。因此,如果你想象一下 调用 代码等待这个任务,它将在 ASP.NET 同步上下文中。 - Jon Skeet
是的,我尝试过了,它仍然在等待CleanSessions()才能继续。它没有完成我想做的事情。 - user1084447

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