我遇到过一个很奇怪的情况,在 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/