如何找出死锁等待的任务及其当前调用堆栈?

big*_*zhu 10 c# debugging visual-studio async-await

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

这个例子的有趣之处在于Hang方法内部没有像Task.Wait()or那样的线程阻塞代码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
Run Code Online (Sandbox Code Playgroud)

主题 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
Run Code Online (Sandbox Code Playgroud)

主线:

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
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*one 7

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

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

数据库

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

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

.load AsyncDebugTools
!dumpasync
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

033a23c8 <0> StackOverflow41476418.Program+<Hang>d__1
Run Code Online (Sandbox Code Playgroud)

输出的描述是:

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

如果异步方法被调用但没有等待,调用者将不会出现在延续堆栈中。

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

LinqPad 和 ClrMd

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

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);
Run Code Online (Sandbox Code Playgroud)

以下是在示例代码上运行的输出示例: 对您的示例代码进行上述查询的结果

点网核心 3

对于 .NET Core 3.x,他们添加!dumpasync到 WinDbg sos 扩展中。它比上述扩展要好得多,因为它提供了更多的上下文。您可以看到它是一个更大的用户故事的一部分,用于改进异步代码的调试。以下是 .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]]
Run Code Online (Sandbox Code Playgroud)