【问题标题】:How to find out deadlocking awaited Tasks and their current call stack?如何找出死锁等待的任务及其当前调用堆栈?
【发布时间】:2017-01-05 02:26:30
【问题描述】:

这是一个简化的示例,我发现在某些情况下很难调试等待任务中的死锁:

class Program
{
    static void Main(string[] args)
    {
        var task = Hang();

        task.Wait();
    }

    static async Task Hang()
    {
        var tcs = new TaskCompletionSource<object>();

        // do some more stuff. e.g. another await Task.FromResult(0);

        await tcs.Task;

        tcs.SetResult(0);
    }
}

这个例子很容易理解为什么它会死锁,它正在等待稍后完成的任务。这看起来很愚蠢,但在更复杂的生产代码中可能会发生类似的情况,并且由于缺乏多线程经验,可能会错误地引入死锁。

这个例子的有趣之处在于Hang 方法内部没有像Task.Wait()Task.Result 这样的线程阻塞代码。然后当我附加 VS 调试器时,它只显示主线程正在等待任务完成。但是,没有线程显示代码在使用并行堆栈视图的Hang 方法中停止的位置。

这是我在并行堆栈中的每个线程(总共 3 个)上的调用堆栈:

头 1:

[Managed to Native Transition]
Microsoft.VisualStudio.HostingProcess.HostProc.WaitForThreadExit
Microsoft.VisualStudio.HostingProcess.HostProc.RunParkingWindowThread
System.Threading.ThreadHelper.ThreadStart_Context
System.Threading.ExecutionContext.RunInternal
System.Threading.ExecutionContext.Run
System.Threading.ExecutionContext.Run
System.Threading.ThreadHelper.ThreadStart

线程 2:

[Managed to Native Transition]
Microsoft.Win32.SystemEvents.WindowThreadProc
System.Threading.ThreadHelper.ThreadStart_Context
System.Threading.ExecutionContext.RunInternal
System.Threading.ExecutionContext.Run
System.Threading.ExecutionContext.Run
System.Threading.ThreadHelper.ThreadStart

主线程:

System.Threading.Monitor.Wait
System.Threading.Monitor.Wait
System.Threading.ManualResetEventSlim.Wait
System.Threading.Tasks.Task.SpinThenBlockingWait
System.Threading.Tasks.Task.InternalWait
System.Threading.Tasks.Task.Wait
System.Threading.Tasks.Task.Wait
ConsoleApplication.Program.Main Line 12 //this is our Main function
[Native to Managed Transition]
[Managed to Native Transition]
System.AppDomain.ExecuteAssembly
Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly
System.Threading.ThreadHelper.ThreadStart_Context
System.Threading.ExecutionContext.RunInternal
System.Threading.ExecutionContext.Run
System.Threading.ExecutionContext.Run
System.Threading.ThreadHelper.ThreadStart

有没有办法找出任务在Hang 方法中停止的位置?如果可能的话,调用堆栈?我相信在内部必须有一些关于每个任务及其延续点的状态,以便调度程序可以工作。但我不知道如何检查。

【问题讨论】:

  • “任务”窗口怎么样?至少它会显示任务已安排,但尚未开始,并且操作是&lt;Hang&gt;d_1 或类似的。
  • @mikez 很高兴您提到了“任务”窗口,我不认为它真的很有帮助。 d_1 只是编译器生成代码中的状态机名称。您可以使用 DotPeek 查看编译器生成的代码。
  • 是的,我意识到这是尽可能少的信息。它只告诉您等待激活的方法的名称(正如您指出的那样由编译器生成),但我认为没有其他任何东西。如果有我很想看看。
  • 我发现来自 Eric Lippert 的 quote 很有趣:“在延续传递风格中,根本没有堆栈,根本没有办法告诉你来自哪里;延续对象没有这些信息. 它只知道你下一步要去哪里. ... 在下一个版本中,如果你使用异步功能,你基本上将放弃基于堆栈的编程;将无法查看调用堆栈并知道你是如何到这里,因为堆栈经常是空的。”

标签: c# visual-studio debugging async-await


【解决方案1】:

在 Visual Studio 内部,我不知道有一种方法可以简单地调试这种情况。但是,对于完整的框架应用程序,还有另外两种可视化方式,另外还有在 .NET Core 3 中执行此操作的方式的奖励预览。

tldr 版本:是的,它很难,而且是的,你想要的信息就在那里,只是很难找到。按照以下方法找到堆对象后,您可以在 VS 监视窗口中使用它们的地址来使用可视化工具进行更深入的研究。

WinDbg

WinDbg 有一个原始但有用的扩展,它提供了一个!dumpasync 命令。

如果您从vs-threading 发布分支下载扩展并将x64 和x86 AsyncDebugTools.dll 复制到C:\Program Files (x86)\Windows Kits\10\Debuggers\[x86|x64]\winext 文件夹,您可以执行以下操作:

.load AsyncDebugTools
!dumpasync

输出(取自上面的链接)如下所示:

07494c7c <0> Microsoft.Cascade.Rpc.RpcSession+<SendRequestAsync>d__49
.07491d10 <1> Microsoft.Cascade.Agent.WorkspaceService+<JoinRemoteWorkspaceAsync>d__28
..073c8be4 <5> Microsoft.Cascade.Agent.WorkspaceService+<JoinWorkspaceAsync>d__22
...073b7e94 <0> Microsoft.Cascade.Rpc.RpcDispatcher`1+<>c__DisplayClass23_2+<<BuildMethodMap>b__2>d[[Microsoft.Cascade.Contracts.IWorkspaceService, Microsoft.Cascade.Common]]
....073b60e0 <0> Microsoft.Cascade.Rpc.RpcServiceUtil+<RequestAsync>d__3
.....073b366c <0> Microsoft.Cascade.Rpc.RpcSession+<ReceiveRequestAsync>d__42
......073b815c <0> Microsoft.Cascade.Rpc.RpcSession+<>c__DisplayClass40_1+<<Receive>b__0>d

在您上面的示例中,输出不太有趣:

033a23c8 <0> StackOverflow41476418.Program+<Hang>d__1

输出的描述是:

上面的输出是一组堆栈——不完全是调用堆栈,实际上是“延续堆栈”。延续堆栈是根据“等待”异步方法调用的代码合成的。异步方法返回的 Task 可能从多个地方等待(例如,Task 存储在一个字段中,然后由多个相关方等待)。当有多个等待者时,堆栈可以分支并显示给定帧的多个后代。因此,上面的堆栈实际上是“树”,每帧的前导点有助于识别树何时有多个分支。

如果调用了异步方法但未等待,则调用者不会出现在继续堆栈中。

一旦您看到更复杂情况的嵌套层次结构,您至少可以深入研究状态对象并找到它们的延续和根源。

LinqPad 和 ClrMd

另一个有用的也是LinqPad 加上ClrMdClrMD.Extensions。后一个包用于将 ClrMd 桥接到 LINQPad - 有一个getting started guide。一旦你设置了包/命名空间,这个查询就是你想要的:

var session = ClrMD.Extensions.ClrMDSession.LoadCrashDump(@"dmpfile.dmp");
var stateMachineTypes = (
    from type in session.Heap.EnumerateTypes()
    where type.Interfaces.Any(item => item.Name == "System.Runtime.CompilerServices.IAsyncStateMachine")
    select type);
session.Heap.EnumerateDynamicObjects(stateMachineTypes).Dump(2);

以下是在您的示例代码上运行的输出示例:

DotNet Core 3

对于 .NET Core 3.x,他们将 !dumpasync 添加到 WinDbg sos 扩展中。它比上面描述的扩展要好得多,因为它提供了更多的上下文。你可以看到它是part of a much larger user story 来改进异步代码的调试。这是 .NET Core 3.0 预览版 6 下的输出,其中包含带有扩展选项的 SOS 预览版 7。请注意,存在行号,这是上述选项所没有的。:

0:000> !dumpasync -stacks -roots
Statistics:
              MT    Count    TotalSize Class Name
00007ffb564e9be0        1           96 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StackOverflow41476418_Core.Program+<Hang>d__1, StackOverflow41476418_Core]]
Total 1 objects
In 1 chains.
         Address               MT     Size      State Description
00000209915d21a8 00007ffb564e9be0       96          0 StackOverflow41476418_Core.Program+<Hang>d__1
Async "stack":
.00000209915d2738 System.Threading.Tasks.Task+SetOnInvokeMres
GC roots:
    Thread bc20:
        000000e08057e8c0 00007ffbb580a292 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2939]
            rbp+10: 000000e08057e930
                ->  00000209915d21a8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StackOverflow41476418_Core.Program+<Hang>d__1, StackOverflow41476418_Core]]
    
        000000e08057e930 00007ffbb580a093 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2878]
            rsi: 
                ->  00000209915d21a8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StackOverflow41476418_Core.Program+<Hang>d__1, StackOverflow41476418_Core]]
    
        000000e08057e9b0 00007ffbb5809f0a System.Threading.Tasks.Task.Wait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2789]
            rsi: 
                ->  00000209915d21a8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StackOverflow41476418_Core.Program+<Hang>d__1, StackOverflow41476418_Core]]
    
Windows symbol path parsing FAILED
        000000e08057ea10 00007ffb56421f17 StackOverflow41476418_Core.Program.Main(System.String[]) [C:\StackOverflow41476418_Core\Program.cs @ 12]
            rbp+28: 000000e08057ea38
                ->  00000209915d21a8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StackOverflow41476418_Core.Program+<Hang>d__1, StackOverflow41476418_Core]]
    
        000000e08057ea10 00007ffb56421f17 StackOverflow41476418_Core.Program.Main(System.String[]) [C:\StackOverflow41476418_Core\Program.cs @ 12]
            rbp+30: 000000e08057ea40
                ->  00000209915d21a8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StackOverflow41476418_Core.Program+<Hang>d__1, StackOverflow41476418_Core]]

【讨论】:

  • 感谢@steven-bone,这是一些令人印象深刻的工具选项,希望它能帮助我下次调试这些死锁!
  • @Steven 这正是我正在寻找的工具...谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多