c# - 等待后任务继续不工作

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

我遇到过一个很奇怪的情况,在 IIS 中 await 之后没有继续执行任务(不确定是否与 IIS 相关)。我使用 Azure 存储和以下 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(在 View 中呈现时),但是如果您多次按 F5,您会看到 _count 正在递增每次刷新后。这意味着由于某种原因没有调用延续。

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

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

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

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

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

最佳答案

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

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

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

所以您并不是真的想要“即发即弃”- 您想要生成响应但让 ASP.NET 忘记它。 ConfigureAwait(false) 的简单解决方案会让每个人都忘记它,这意味着一旦发生在蓝色月亮上,您的延续可能会“丢失”。

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

如果您不打算执行理想的解决方案,那么您将前往 live dangerously .我的博文中有代码会向 ASP.NET 运行时注册 Task,这样您就可以尽早返回响应,但会通知 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));
    }
}

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

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

关于c# - 等待后任务继续不工作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13992750/

相关文章:

c# - 在 ASP.NET MVC 中安全和 "squarified"照片上传

c# - 对如何构造调用 SmtpClient.SendMailAsync() 的异步/等待代码感到困惑

c# - 使用 TPL 创建套接字集合是否太过分了,或者是否有更好的方法来创建多个套接字?

c# - ResourceManager 没有选择正确的语言

c# - 有 1 个 pKey 和 2 个属性的组合键可以吗

c# - 在父类(super class)部分 View 中使用子类 UI 提示

c# - 无法识别工具版本 "15.0"- Visual Studio 2019 (v16.0.0 Preview 5.0) 中的项目不兼容/已卸载

c# - Async/Await 或 Task.Run 在控制台应用程序/Windows 服务中

c# - Google 不允许网络客户端吗?

c# - 在 Windows 上启动 MAUI 应用程序 exe 的最简单方法?