如何修复具有同步上下文的线程池线程上的死锁



使用HttpClient发送 http 请求时,我会遇到间歇性死锁,有时它们永远不会返回到我的代码中的await SendAsync。我能够在HttpClient/HttpClientHandler中找出内部处理请求的线程,由于某种原因在死锁期间有SynchronizationContext。我想弄清楚使用的线程如何以SynchronizationContext结束,而通常他们没有。我假设导致设置此SynchronizationContext的任何对象也在阻塞Thread,从而导致死锁。

我能否在 TPL ETW 事件中看到任何相关内容?

如何对此进行故障排除?



编辑2:我注意到这些死锁的地方是在Windows服务内部的wcfServiceContract(见下面的代码)。导致问题的SynchronizationContext实际上是一个WindowsFormsSynchronizationContext,我认为这是由创建某些控件并且未正确清理(或类似的东西)引起的。我意识到几乎肯定不应该在Windows服务中发生任何Windows表单的东西,我并不是说我同意它的使用方式。但是,我没有使用它编写任何代码,而且我不能简单地更改所有引用。

编辑:这是我遇到问题的WCF服务的一般想法的示例。这是一个简化版本,而不是确切的代码:

[ServiceContract]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
internal class SampleWcfService
{
private readonly HttpMessageInvoker _invoker;
public SampleWcfService(HttpMessageInvoker invoker)
{
_invoker = invoker;
}

[WebGet(UriTemplate = "*")]
[OperationContract(AsyncPattern = true)]
public async Task<Message> GetAsync()
{
var context = WebOperationContext.Current;
using (var request = CreateNewRequestFromContext(context))
{
var response = await _invoker.SendAsync(request, CancellationToken.None).ConfigureAwait(false);
var stream = response.Content != null ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null;
return StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream());
}
}
}

ConfigureAwait(false)添加到上面的 2 个地方并没有完全解决我的问题,因为用于为进入此处的 wcf 请求提供服务的线程池线程可能已经具有SynchronizationContext在这种情况下,请求会一直完成整个GetAsync方法并返回。但是,它仍然最终在System.ServiceModel.Dispatcher.TaskMethodInvoker中陷入僵局,因为在微软代码中,它不使用ConfigureAwait(false),我想假设有一个很好的理由(供参考):

var returnValueTask = returnValue as Task;
if (returnValueTask != null)
{
// Only return once the task has completed                        
await returnValueTask;
}

感觉真的很不对劲,但是将其转换为使用APM(开始/结束)而不是使用任务可以解决此问题吗?或者,唯一的解决方法是更正未正确清理其SynchronizationContext的代码?

更新:我们现在知道我们正在处理一个WindowsFormsSynchronizationContext(请参阅注释),无论出于何种原因,在 WCF 应用程序中。看到死锁也就不足为奇了,因为 SyncContext 的重点是在同一线程上运行所有延续。

您可以尝试将 WindowsFormsSynchronizationContext.AutoInstall 设置为false。根据其文档,它的作用是:

获取或设置一个值,该值指示在创建控件时是否安装了 WindowsFormsSynchronizationContext

假设有人在你的应用中的某个位置创建了 WindowsForms 控件,那么这可能是你的问题,并且可能会通过禁用此设置来解决。

摆脱现有SynchronizationContext另一种方法是用 null 覆盖它,然后恢复它(如果你很好的话)。本文介绍此方法,并提供可以使用的方便SynchronizationContextRemover实现。

但是,如果 SyncContext 是由您使用的某些库方法创建的,则这可能不起作用。我不知道有什么方法可以防止 SyncContext 被覆盖,因此设置虚拟上下文也无济于事。


你确定SynchronizationContext真的有错吗?

来自这篇 MSDN 杂志文章:

默认 (ThreadPool) SynchronizationContext

(mscorlib.dll: System.Threading)默认
SynchronizationContext 是默认构造的 SynchronizationContext 对象。按照惯例,如果线程的当前 SynchronizationContext为 null,则它隐式具有默认的 SynchronizationContext

默认的 SynchronizationContext 将其异步委托排队到 ThreadPool,但直接在调用线程上执行其同步委托。因此,其上下文涵盖所有线程池线程以及调用 Send 的任何线程。上下文"借用"调用 Send 的线程,将它们引入其上下文,直到委托完成。从这个意义上说,默认上下文可以包含进程中的任何线程。

默认的 SynchronizationContext 将应用于 ThreadPool 线程,除非代码由 ASP.NET 托管。默认的同步上下文也隐式应用于显式子线程(线程类的实例),除非子线程设置自己的同步上下文。

如果您看到的SynchronizationContext是默认的,那么它应该没问题(或者更确切地说,您将很难避免使用它)。

您不能提供有关所涉及的内容的更多详细信息/代码吗?

在您的代码中,对我来说立即可疑的一件事(尽管可能完全没问题)是,您有一个using块,可以捕获request中的静态WebOperationContext.Current,这两者都将被生成的异步状态机捕获。同样,可能没问题,但如果有什么东西在等待WebOperationContext,这里有很多死锁的可能性

试试下面;我在类似的案例中发现了进入异步兔子洞的成功。

var responsebytes = await response.Content.ReadAsByteArrayAsync();
MemoryStream stream = new MemoryStream(filebytes);

响应流变量。

希望对您有所帮助。

最新更新