我在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);
}
一切都按预期开始工作,除了我不想让客户端等待我的异步操作完成。
所以我的问题是:我想了解为什么会发生这种情况,以及运行“即发即忘”工作的一致方法是什么,最好不要跨越新线程。
答案 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
方法的末尾,你将让它在一个线程池线程中运行,这对你的情况应该没问题。
其他选项是从任务中抛出异常,或者异步操作根本没有完成。