等待正在后台线程上完成的任务会导致请求线程挂起



我有一个面向 .NET 4.6 的 ASP.NET 应用程序,其中我编写了一个全局"事件监视器"。这个单例的目的是双重的:

  1. 正常的请求处理管道向其报告各种业务事件。这些事件被放置在专用后台线程的队列中,该线程 a) 将它们写入数据库,b) 将它们转发给任何感兴趣的观察者(参见 #2)。

  2. 另一方面,管理请求(由管理会话发出的一种特殊类型的长轮询 HTTP 请求)可以选择观察或侦听在非常接近实时的正常请求中发生的某些类型的事件(因此称为"事件监视"),这些事件基于事件过滤器的组合(例如无效的登录尝试或敏感业务数据的修改, 等)然后对他们做出反应。

观察也可以由任何希望监视自己的事件的正常请求(即自我观察)来执行。例如,涉及从数据库读取的请求可能希望保留有关正在读取的确切内容和顺序的高级选项卡。这在开发和调试期间特别有用,因为它不涉及单独的管理请求,只是为了找出正在向数据库发出哪些 SQL 语句或正在读取哪些文件。在尝试拼凑复杂请求处理所需的所有内容时,此类信息非常宝贵。

我目前遇到的问题是这种自我观察。请求管道一直使用异步,并且没有任何问题。也就是说,直到我进入等待观察到的事件。

这是我编写的基本 API:

var observer = new EventObserver();
using (EventMonitor.Instance.Observe(...params..., observer))
{
await MyComplexBusinessWorkThatReportsManyEvents();
}
var events = await observer.Task;
Debug.WriteLine(events.ToJson());

如您所见,观察者 API 也是异步的。当到达using结束时,生成的Dispose调用会将观察者与监视器分离。但是,由于报告的事件是在后台线程中带外处理的,因此可能只是略微不同步,我现在必须等待分离的观察者,直到分离时间之前的所有事件都涓涓细流。这通常发生得非常快,但仍然 - 足够慢,以至于必须等待。

observer.Task实际上是TaskCompletionSource.Task的封装,它从后台线程(处理队列中所有报告事件的线程)完成。这就是代码挂起的地方。现在,我意识到我可以以不同的方式编写此 API......我本可以使用通常的线程同步原语,例如ManualResetEvent.但由于这意味着我必须阻止请求线程,所以我选择了TaskCompletionSource和 async/await。这纯粹是一个设计决定,而不是必需品。

当然,一旦我进行以下更改,挂起就会消失:

var events = await observer.Task.ConfigureAwait(false);

这告诉我问题与捕获的上下文以及任务正在不同线程上完成的事实有关。显式ConfigureAwait(false)调用本身不会有问题,如果只需要写一次:在上面的行中。不幸的是,我已经看到我必须在等待链中一直携带它。换句话说,只有当 ALL 从请求管道的开始等待到指定ConfigureAwait(false)await observer.Task时,挂起才会消失。

我意识到我一定在做傻事,但这让我感到困惑。我希望专家的解释...我错过了什么?

我自己找到了愚蠢的部分。事实证明,我并没有像我最初声称的那样完全异步。咚!

因此,黄金法则再次被证明:不要试图将异步硬塞到非异步代码中!

最新更新