等待后任务继续不起作用 [英] Task continuation not working after await

查看:27
本文介绍了等待后任务继续不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我遇到了一个很奇怪的情况,在 IIS 中 await 之后任务没有继续执行(不确定是否与 IIS 相关).我使用 Azure 存储和以下控制器重现了这个问题(github 上的完整解决方案):

I've encountered a pretty strange case when task execution is not continued after await in IIS (not sure if it's related to IIS). I reproduced this issue using Azure Storage and following controller (full solution on github):

public class HomeController : Controller
{
    private static int _count;

    public ActionResult Index()
    {
        RunRequest(); //I don't want to wait on this task
        return View(_count);
    }

    public async Task RunRequest()
    {
        CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
        var cloudTable = account.CreateCloudTableClient().GetTableReference("test");

        Interlocked.Increment(ref _count);
        await Task.Factory.FromAsync<bool>(cloudTable.BeginCreateIfNotExists, cloudTable.EndCreateIfNotExists, null);

        Trace.WriteLine("This part of task after await is never executed");
        Interlocked.Decrement(ref _count);
    }
}

我希望 _count 的值始终为 1(在视图中呈现时),但是如果您多次按 F5,您会看到 _count 正在递增每次刷新后.这意味着出于某种原因不会调用继续.

I would expect the value of _count to be always 1 (when rendered in view), but if you hit F5 several time you'll see that _count is incrementing after each refresh. That means that continuation is not called for some reason.

实际上我撒了一点谎,我注意到当第一次调用 Index 时,会调用一次 continuation.所有进一步的 F5 不会减少计数器.

In fact I've lied a bit, I've noticed that continuation is called once, when Index is called for the first time. All further F5's don't decrement the counter.

如果我将方法更改为异步:

If I change the method to be async:

    public async Task<ActionResult> Index()
    {
        await RunRequest(); //I don't want to wait on this task
        return View(_count);
    }

一切都按预期开始工作,只是我不想让客户端等待我的异步操作完成.

everything starts working as expected except that I don't want to keep client waiting for my asynchronous operation to finish.

所以我的问题是:我想了解为什么会发生这种情况,以及运行即发即弃"工作的一致方式是什么,最好不要跨越新线程.

So my question is: I would like to understand why this happens, and what is the consistent way to run "fire and forget" work, preferably without spanning new threads.

推荐答案

什么是运行即发即忘"工作的一致方式

what is the consistent way to run "fire and forget" work

ASP.NET 不是为即发即忘的工作而设计的;它旨在为 HTTP 请求提供服务.当生成 HTTP 响应时(当您的操作返回时),该请求/响应周期就完成了.

ASP.NET was not designed for fire-and-forget work; it was designed to serve HTTP requests. When an HTTP response is generated (when your action returns), that request/response cycle is complete.

请注意,只要没有活动请求,ASP.NET 将随时删除您的 AppDomain.这通常是在非活动超时后在共享主机上完成的,或者当您的 AppDomain 进行了一定数量的垃圾收集时,或者每 29 小时无缘无故地进行一次.

Note that ASP.NET will feel free to take down your AppDomain any time that there are no active requests. This is normally done on shared hosts after an inactivity timeout, or when your AppDomain has had a certain number of garbage collections, or every 29 hours just for no reason at all.

所以你并不真的想要即发即忘"——你想要产生响应但不是让 ASP.NET 忘记它.ConfigureAwait(false) 的简单解决方案会让每个人都忘记它,这意味着一旦进入蓝月亮,您的延续可能会丢失".

So you don't really want "fire and forget" - you want to produce the response but not have ASP.NET forget about it. The simple solution of ConfigureAwait(false) will cause everyone to forget about it, which means that once in a blue moon your continuation could just get "lost".

我有一篇博文更详细地介绍了这个主题.简而言之,您希望在生成响应之前在持久层(如 Azure 表)中记录要完成的工作.这是理想的解决方案.

I have a blog post that goes into more detail on this subject. In short, you want to record the work to be done in a persistent layer (like an Azure table) before your response is generated. That's the ideal solution.

如果您不打算采用理想的解决方案,那么您将危险地生活.在我的博文中有一段代码会在 ASP.NET 运行时注册 Tasks,这样你就可以尽早返回响应,但通知 ASP.NET 你不是真的em> 完成了.这将防止 ASP.NET 在您有出色的工作时关闭您的网站,但它不会保护您免受更基本的故障的影响,例如硬盘崩溃或有人被服务器的电源线绊倒.

If you aren't going to do the ideal solution, then you're going to live dangerously. There is code in my blog post that will register Tasks with the ASP.NET runtime, so that you can return a response early but notify ASP.NET that you're not really done yet. This will prevent ASP.NET from taking down your site while you have outstanding work, but it will not protect you against more fundamental failures like a hard drive crash or someone tripping over the power cord of your server.

我博客文章中的代码复制如下;它取决于我的 AsyncEx 库中的 AsyncCountdownEvent:

The code in my blog post is duplicated below; it depends on the AsyncCountdownEvent in my AsyncEx library:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Hosting;
using Nito.AsyncEx;

/// <summary>
/// A type that tracks background operations and notifies ASP.NET that they are still in progress.
/// </summary>
public sealed class BackgroundTaskManager : IRegisteredObject
{
    /// <summary>
    /// A cancellation token that is set when ASP.NET is shutting down the app domain.
    /// </summary>
    private readonly CancellationTokenSource shutdown;

    /// <summary>
    /// A countdown event that is incremented each time a task is registered and decremented each time it completes. When it reaches zero, we are ready to shut down the app domain. 
    /// </summary>
    private readonly AsyncCountdownEvent count;

    /// <summary>
    /// A task that completes after <see cref="count"/> reaches zero and the object has been unregistered.
    /// </summary>
    private readonly Task done;

    private BackgroundTaskManager()
    {
        // Start the count at 1 and decrement it when ASP.NET notifies us we're shutting down.
        shutdown = new CancellationTokenSource();
        count = new AsyncCountdownEvent(1);
        shutdown.Token.Register(() => count.Signal(), useSynchronizationContext: false);

        // Register the object and unregister it when the count reaches zero.
        HostingEnvironment.RegisterObject(this);
        done = count.WaitAsync().ContinueWith(_ => HostingEnvironment.UnregisterObject(this), TaskContinuationOptions.ExecuteSynchronously);
    }

    void IRegisteredObject.Stop(bool immediate)
    {
        shutdown.Cancel();
        if (immediate)
            done.Wait();
    }

    /// <summary>
    /// Registers a task with the ASP.NET runtime.
    /// </summary>
    /// <param name="task">The task to register.</param>
    private void Register(Task task)
    {
        count.AddCount();
        task.ContinueWith(_ => count.Signal(), TaskContinuationOptions.ExecuteSynchronously);
    }

    /// <summary>
    /// The background task manager for this app domain.
    /// </summary>
    private static readonly BackgroundTaskManager instance = new BackgroundTaskManager();

    /// <summary>
    /// Gets a cancellation token that is set when ASP.NET is shutting down the app domain.
    /// </summary>
    public static CancellationToken Shutdown { get { return instance.shutdown.Token; } }

    /// <summary>
    /// Executes an <c>async</c> background operation, registering it with ASP.NET.
    /// </summary>
    /// <param name="operation">The background operation.</param>
    public static void Run(Func<Task> operation)
    {
        instance.Register(Task.Run(operation));
    }

    /// <summary>
    /// Executes a background operation, registering it with ASP.NET.
    /// </summary>
    /// <param name="operation">The background operation.</param>
    public static void Run(Action operation)
    {
        instance.Register(Task.Run(operation));
    }
}

它可以像这样用于 async 或同步代码:

It can be used like this for async or synchronous code:

BackgroundTaskManager.Run(() =>
{
    // Synchronous example
    Thread.Sleep(20000);
});
BackgroundTaskManager.Run(async () =>
{
    // Asynchronous example
    await Task.Delay(20000);
});

这篇关于等待后任务继续不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆