【问题标题】:How do I fix the deadlock on a threadpool thread that has a SynchronizationContext?如何修复具有 SynchronizationContext 的线程池线程上的死锁?
【发布时间】:2018-03-16 00:09:04
【问题描述】:

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

我能在 TPL ETW 活动中看到任何相关内容吗?

我该如何解决这个问题?



编辑 2: 我注意到这些死锁的地方是在 Windows 服务内的 wcf ServiceContract(参见下面的代码)中。导致问题的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) 添加到上述两个位置并不能完全解决我的问题,因为用于服务进入此处的 wcf 请求的线程池线程可能已经具有SynchronizationContext在这种情况下,请求会一直通过整个GetAsync 方法并返回。然而,它仍然在System.ServiceModel.Dispatcher.TaskMethodInvoker 中陷入僵局,因为在那个微软代码中,它没有使用ConfigureAwait(false),我想假设这是有充分理由的(for reference):

var returnValueTask = returnValue as Task;

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

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

【问题讨论】:

  • 你有话题吗?你怎么没贴代码?
  • @CodingYoshi 我没有什么可以补充的,我还没有在问题中说。我可以看出SynchronizationContext 是基于进程转储在问题线程中设置的,但该线程没有托管堆栈跟踪,只有非托管的东西。
  • 某事物正在启动 SynchronizationContext 然后等待它完成(这永远不会发生)的理论是可靠的。但是,如果您不向我们展示代码,我们只能告诉您这么多。
  • 您是一直在等待所有任务,还是在某个时候阻塞?如果是后者,要么更改停止阻塞并等待,要么在您的任务上使用 ConfigureAwait(false):blog.stephencleary.com/2012/07/dont-block-on-async-code.html
  • @ejohnson 我能够通过使用 ConfigureAwait(false) 让它停止死锁。但我仍在试图弄清楚SynchronizationContext 是如何设置的,然后我将根据此决定使用ConfigureAwait(false) 是否有意义。此外,我想确保添加 ConfigureAwait(false) 不会添加任何意外的副作用,即使它似乎有助于这种情况。

标签: c# .net async-await task-parallel-library synchronizationcontext


【解决方案1】:

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

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

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

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

一种替代方法来摆脱现有的 SynchronizationContext 将只是用 null 覆盖它,然后再恢复它(如果你很好的话)。这个article 描述了这种方法并提供了一个方便的SynchronizationContextRemover 实现,您可以使用。

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


您确定SynchronizationContext 确实有问题吗?

来自MSDN magazine article

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

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

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

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

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

在您的代码中,我立即觉得可疑的一件事(尽管它可能完全没问题)是您有一个 using 块,它在 request 中捕获了一个静态 WebOperationContext.Current,这两者都将被生成异步状态机。再说一次,可能没问题,但如果有东西在 WebOperationContext 上等待,这里很有可能出现死锁@

【讨论】:

  • 至于using 相对于WebOperationContext,它所做的只是将其中的一些内容复制到新创建的HttpRequestMessage,我认为这不会造成问题.
  • 另外,我相信我有充分的理由认为SynchronizationContext 是问题所在。根据我添加的跟踪,通常线程会以空值SynchronizationContext 通过那里,并且只有具有SynchronizationContext.Current 而不是null 的线程才会挂在我在问题中提到的位置:await returnValueTask;
  • 我并不是说我们的代码中没有错误导致SynchronizationContext 间歇性出现,因为可能存在。我试图弄清楚我是否可以调整这个 wcf 服务和相邻的代码来解决这个问题(或者弄清楚实现是否存在固有的错误),因为我发布的这些代码实际上都没有使用线程的 @987654344 @反正。
  • “等待WebOperationContext”是什么意思?
  • 我要说的是尝试确定添加的SynchronizationContext 是否是默认值(它只是称为“SynchronizationContext”,而不是例如AspNetSynchronizationContext)。如文章中所述,如果不存在任何内容,则可以通过各种方式设置此上下文。这意味着很难追踪它的引入位置,也很难避免它被添加。
【解决方案2】:

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

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

响应流变量。

希望对你有帮助。

【讨论】:

  • 有机会我会试试的
  • 你的意思是new MemoryStream(responsebytes)
  • 这不会改变任何东西;我的意思是ReadAsStreamAsync 本质上在内部做同样的事情,只是添加了一些缓冲。
  • @CShark 是的,抱歉,新的 MemoryStream(responsebytes)。我打错字了。在您的代码示例中,您使用了两个等待,这是正确的,但请尝试: var stream = response.Content != null ?等待 response.Content.ReadAsStreamAsync().Result : null;您仍然有一个 await ,对于 async 方法来说应该足够了。如果这不起作用 - 我已经完成并且很抱歉。谢谢你的尝试。
  • 首先,这不会编译:var stream = response.Content != null ? await response.Content.ReadAsStreamAsync().Result : null;。您正在尝试await 此代码中的结果,而不是Task。此外,使其同步甚至无法避免我提到的死锁,甚至可能引入更多问题。
猜你喜欢
  • 2010-09-10
  • 2015-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多