等待后台线程上正在完成的任务会导致请求线程挂起 [英] Awaiting a task that is being completed on a background thread causes the request thread to hang

查看:204
本文介绍了等待后台线程上正在完成的任务会导致请求线程挂起的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have an ASP.NET app targeting .NET 4.6 in which I coded a global "event monitor". The purpose of this singleton is two-fold:

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

  1. The normal request processing pipeline reports various business events to it. These events are put in a queue of a dedicated background thread which, a) writes them to a database, and b) forwards them to any interested observers (see #2).

管理请求(由管理会话发出的一种特殊的长轮询HTTP请求)可以选择观察或侦听在非常接近实时的普通请求中发生的某些类型的事件(因此称为事件监视"),然后结合事件过滤器(例如无效的登录尝试或敏感业务数据的修改等)进行处理.

Administrative requests (a special kind of long-polling HTTP request made by administrative session) on the other hand, may choose to observe or listen to certain types of events that happen in normal requests very close to real-time (hence the term "event monitoring") based on a combination of event filters (e.g. invalid log-in attempts, or modification of sensitive business data, etc.) and then react to them.

观察也可以由希望监视其自身事件(即自我观察)的任何正常请求执行.例如,涉及从数据库读取的请求可能希望在高级选项卡上保留确切的读取内容和读取顺序.这在开发和调试期间特别有用,因为它不涉及单独的管理请求,而只是查找要向数据库发出哪些SQL语句或正在读取哪些文件.当试图将复杂的请求处理所需要的所有内容组合在一起时,这样的信息是非常宝贵的.

Observing can also be performed by any normal request that wishes to monitor its own events (i.e. to self-observe). For example, a request that involves reading from a database might want to keep high-level tabs on what exactly is being read and in what order. This is especially useful during development and debugging, because it doesn't involve a separate administrative request just to find out which SQL statements are being issued to the database or which files are being read. Information like this is invaluable when trying to piece together everything that complex request handling entails.

我目前遇到的问题是这种自我观察.请求管道一直使用异步,并且可以正常工作.也就是说,直到我进入等待观察到的事件为止.

The problem I'm currently having is with this self-observing. The request pipeline uses async all the way and works without any problems. That is, until I enter awaiting of observed events.

这是我编写的基本API:

Here's the basic API that I coded:

var observer = new EventObserver();
using (EventMonitor.Instance.Observe(...params..., observer))
{
    await MyComplexBusinessWorkThatReportsManyEvents();
}

var events = await observer.Task;
Debug.WriteLine(events.ToJson());

如您所见,观察者API也是异步的.到达using的结尾时,对Dispose的最终调用将使观察者与监视器分离.但是,由于报告的事件是在后台线程中进行带外处理的,因此可能会略有不同步,因此,我现在必须等待分离的观察者,直到所有事件达到分离时间为止.这通常很快发生,但仍然-足够慢,必须等待.

As you can see, the observer API is async as well. When the end of using is reached, the resulting call to Dispose detaches the observer from the monitor. However, since the reported events are processed out of band in a background thread and might therefore be just slightly out of sync, I now have to await the detached observer until all the events up to detachment time trickle in. This usually happens very quickly, but still - slow enough that it has to be awaited.

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

The observer.Task is really an encapsulation of a TaskCompletionSource.Task, which gets completed from a background thread (the one that processes all reported events in queue). And this is where the code hangs. Now, I realize I could have coded this API differently... I could have used the usual thread synchronization primitives, such as ManualResetEvent. But since that means I'd have to block the request thread, I opted for TaskCompletionSourceand async/await. It was purely a design decision, not a necessity.

当然,我进行了以下更改后,挂起便消失了:

Of course, the hang goes away as soon as I make the following change:

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

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

Which tells me the problem has to do with the captured context and the fact that the task is being completed on a different thread. The explicit ConfigureAwait(false) call itself wouldn't be a problem, if all it took was writing it once: in the line above. Unfortunately, I've seen that I have to carry it all the way up in the await chain. In other words, the hang only goes away when ALL awaits from the start of request pipeline down to the await observer.Task specify ConfigureAwait(false).

我意识到我一定在做一些愚蠢的事情,但这使我感到困惑.我希望得到专家的解释...我想念什么?

I realize I must be doing something stupid, but this baffles me. I'd appreciate an explanation from experts... What did I miss?

推荐答案

我自己找到了愚蠢的部分.我发现我一开始并没有完全异步. h!

Found the stupid part on my own. I turns out I didn't exactly have async all the way, as I originally claimed. Doh!

因此,黄金法则再次得到了证明:不要尝试将鞋拔异步到非异步代码中!

And so the golden rule has been proven once more: DO NOT try and shoehorn async into non-async code!

这篇关于等待后台线程上正在完成的任务会导致请求线程挂起的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆