等待后任务继续不起作用

时间:2012-12-21 15:25:38

标签: c# asp.net-mvc task-parallel-library azure-storage .net-4.5

我在IIS中await之后没有继续执行任务时遇到了一个非常奇怪的情况(不确定它是否与IIS有关)。我使用Azure存储和跟随控制器(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递增。这意味着由于某种原因不会调用延续。

事实上我已经撒了一点,我注意到,当第一次调用Index时,会继续调用一次延续。所有进一步的F5都不会减少计数器。

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

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

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

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

3 个答案:

答案 0 :(得分:3)

  

运行“一劳永逸”工作的一致方法是什么

ASP.NET并非专为“即发即弃”工作而设计;它旨在提供HTTP请求。当生成HTTP响应时(当您的操作返回时),该请求/响应周期就完成了。

请注意,只要没有活动请求,ASP.NET就可以随时关闭AppDomain。这通常在不活动超时后,或者当您的AppDomain有一定数量的垃圾收集时,或者在没有任何理由的情况下每29小时在共享主机上完成。

所以你真的不想“开火而忘记” - 你想要产生响应,但让ASP.NET忘记了它。 ConfigureAwait(false)的简单解决方案将导致每个人忘记它,这意味着一旦在蓝色的月亮中,你的延续可能会“迷失”。

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

如果您不打算做出理想的解决方案,那么您将前往live dangerously。我的博客文章中有代码将Task注册到ASP.NET运行时,以便您可以提前返回响应但通知ASP.NET您 已完成然而。这将阻止ASP.NET在您完成工作时关闭您的站点,但它不会保护您免受更严重的故障,如硬盘驱动器崩溃或有人绊倒服务器的电源线。

我博客文章中的代码重复如下;这取决于AsyncEx library中的AsyncCountdownEvent

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或同步代码一样使用:

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

答案 1 :(得分:2)

嗯,你必须有一个线程某处执行延续。我怀疑问题是在awaiter中捕获的上下文“知道”请求已经完成。我不知道在那种情况下会发生什么的细节,但它可能只是忽略了任何延续。听起来确实听起来有点奇怪......

你可以尝试使用:

await Task.Factory.FromAsync<bool>(cloudTable.BeginCreateIfNotExists,
                                   cloudTable.EndCreateIfNotExists, null)
          .ConfigureAwait(false);

这样它就不会尝试继续捕获的上下文,而是仅仅依赖于任意的线程池线程。 可能没有帮助,但值得一试。

答案 2 :(得分:1)

问题是await将继续配置为在首次启动它的同步上下文中运行。这实际上是功能的一个更有用的方面,但在这种情况下它对你来说是一个问题。此处,在继续触发时,您的同步上下文不存在,因为您正在返回视图。

我的猜测是,尝试访问已经“完成”的同步上下文会导致抛出异常,这就是您的代码无效的原因。

如果你将ConfigureAwait(false)添加到FromAsync方法的末尾,你将让它在一个线程池线程中运行,这对你的情况应该没问题。

其他选项是从任务中抛出异常,或者异步操作根本没有完成。