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

时间:2016-12-06 09:35:24

标签: c# asp.net multithreading asynchronous async-await

我有一个针对.NET 4.6的ASP.NET应用程序,其中我编写了一个全局"事件监视器"。这个单身人士的目的有两个:

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

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

    < / LI>

    观察也可以由希望监视其自身事件(即自我观察)的任何正常请求来执行。例如,涉及从数据库读取的请求可能希望保留高级选项卡,以确定正在读取的内容和顺序。这在开发和调试期间特别有用,因为它不涉及单独的管理请求,只是为了找出正在向数据库发出哪些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)调用本身不会成为问题,如果只需要编写一次:在上面的行中。不幸的是,我已经看到我必须在await链中一直带着它。换句话说,当所有等待从请求管道开始到await observer.Task指定ConfigureAwait(false)时,挂起才会消失。

    我意识到我必须做一些愚蠢的事情,但这让我感到困惑。我很欣赏专家的解释......我错过了什么?

1 个答案:

答案 0 :(得分:0)

自己找到stupid part。事实证明,我并没有完全像我原先声称的那样异步。卫生署!

因此黄金法则再次得到证明:不要试图将非同步代码中的异步同步变为非同步代码!