2

我遇到了一个非常奇怪的情况,即await在 IIS 之后任务执行没有继续(不确定它是否与 IIS 有关)。我使用 Azure 存储和以下控制器重现了这个问题(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);
    }

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

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

4

3 回答 3

3

运行“一劳永逸”工作的一贯方式是什么

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

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

所以你并不是真的想要“一发不可收拾”——你想要产生响应但又不想让ASP.NET 忘记它。的简单解决方案ConfigureAwait(false)会让每个人都忘记它,这意味着一旦在一个蓝月亮,你的延续可能会“迷失”。

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

如果您不打算做理想的解决方案,那么您将过着危险的生活。在我的博客文章中有代码将Tasks 注册到 ASP.NET 运行时,以便您可以提前返回响应,但通知 ASP.NET 您还没有真正完成。这将防止 ASP.NET 在您有出色的工作时关闭您的站点,但它不会保护您免受更根本的故障,例如硬盘驱动器崩溃或有人绊倒您的服务器电源线。

我的博客文章中的代码在下面重复;这取决于AsyncCountdownEvent我的AsyncEx 库中的:

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);
});
于 2012-12-21T16:16:52.327 回答
2

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

您可以尝试使用:

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

这样它就不会尝试在捕获的上下文上继续,而是在任意线程池线程上继续。它可能无济于事,但值得一试。

于 2012-12-21T15:34:02.627 回答
2

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

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

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

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

于 2012-12-21T15:34:29.523 回答