【发布时间】:2016-12-09 02:14:07
【问题描述】:
我有一个面向 .NET 4.6 的 ASP.NET 应用程序,我在其中编写了一个全局“事件监视器”。这个单例的目的有两个:
正常的请求处理管道向它报告各种业务事件。这些事件被放入一个专用后台线程的队列中,a) 将它们写入数据库,b) 将它们转发给任何感兴趣的观察者(参见 #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和异步/等待。这纯粹是一个设计决定,而不是必需品。
当然,只要我进行以下更改,问题就会消失:
var events = await observer.Task.ConfigureAwait(false);
这告诉我问题与捕获的上下文以及任务正在不同线程上完成的事实有关。显式的 ConfigureAwait(false) 调用本身不会有问题,如果它只需要编写一次:在上面的行中。不幸的是,我已经看到我必须在等待链中一直携带它。换句话说,只有当 ALL 从请求管道开始一直等待到 await observer.Task 指定 ConfigureAwait(false) 时,挂起才会消失。
我意识到我一定是在做一些愚蠢的事情,但这让我感到困惑。非常感谢专家的解释...我错过了什么?
【问题讨论】:
-
你能发布一个简单而完整的例子来重现这个问题吗?
-
是的,但由于 ASP.NET 上下文依赖关系,这并不容易。我基本上必须用最少的代码重现一个独立项目中的所有主要方面。明天试试!
标签: c# asp.net multithreading asynchronous async-await