获取 async/await 应用程序中的所有堆栈跟踪

Get ALL stacktraces in async/await application

我想获取有关我的异步 C# 应用程序中所有调用堆栈的信息(或获取所有堆栈跟踪)。我知道,how to get stacktraces of all existing threads

但是如何获取有关 await 释放的所有调用堆栈的信息,其中没有 运行 线程?


上下文示例

假设如下代码:

private static async Task Main()
{
    async Task DeadlockMethod(SemaphoreSlim lock1, SemaphoreSlim lock2)
    {
        await lock1.WaitAsync();
        await Task.Delay(500);
        await lock2.WaitAsync(); // this line causes the deadlock
    }

    SemaphoreSlim lockA = new SemaphoreSlim(1);
    SemaphoreSlim lockB = new SemaphoreSlim(1);

    Task call1 = Task.Run(() => DeadlockMethod(lockA, lockB));
    Task call2 = Task.Run(() => DeadlockMethod(lockB, lockA));

    Task waitTask = Task.Delay(1000);

    await Task.WhenAny(call1, call2, waitTask);

    if (!call1.IsCompleted
        && !call2.IsCompleted)
    {
        // DUMP STACKTRACES to find the deadlock
    }
}

我想转储所有堆栈跟踪,即使是那些当前没有线程的堆栈跟踪,以便我可以找到死锁。

如果行 await lock2.WaitAsync(); 更改为 lock2.Wait();,则可以通过已经提到的 get stacktraces of all threads。但是 如何在没有 运行 线程的情况下列出所有堆栈跟踪?

防止误解:

I know, how to get stacktraces of all existing threads.

这里只是介绍一下背景。

在Windows中,线程是一个OS概念。它们是调度的单位。所以某处有一个明确的线程列表,因为那是 OS 调度程序使用的。

此外,每个线程都有一个调用栈。这可以追溯到计算机编程的早期。然而,调用栈的目的经常被误解。调用堆栈用作 return 个位置的序列。当方法 returns 时,它会从堆栈中弹出其调用堆栈参数以及 return 位置,然后跳转到 return 位置。

记住这一点很重要,因为调用堆栈 并不表示代码如何进入某种情况; 它表示 代码的去向 returns 来自当前方法。调用堆栈是代码要去的地方,而不是它来自的地方。这就是调用堆栈存在的原因:指导未来的代码,而不是协助诊断。现在,事实证明,调用堆栈确实包含对诊断有用的信息,因为它指示了代码来自哪里以及它要去哪里,所以这就是调用的原因堆栈处于异常状态,通常用于诊断。但这并不是调用堆栈存在的真正原因;这只是一个快乐的情况。

现在,输入异步代码。

在异步代码中,调用堆栈仍然表示代码 returning 的位置(就像所有调用堆栈一样)。但在异步代码中,调用堆栈 不再表示代码 来自 的地方 。在同步的世界里,这两个东西是一样的,调用栈(这是必须的)也可以用来回答“这段代码是怎么到这里来的?”的问题。在异步世界中,调用堆栈仍然是必需的,但只是回答了“这段代码要去哪里?”的问题。并且不能回答“这段代码是怎么来的?”这个问题。回答“这段代码是怎么来的?”问题你需要 causality chain.

此外,调用堆栈是正确操作所必需的(在同步和异步世界中),因此 compiler/runtime 确保它们存在。因果链 不是 必需的,它们不是开箱即用的。在同步世界中,调用堆栈恰好是一个因果链,这很好,但这种愉快的情况不会延续到异步世界。

When a thread is released by await, the stacktrace and all objects along the call stack are stored somewhere.

否;不是这种情况。如果 async 使用光纤,这将是正确的,但事实并非如此。任何地方都没有保存调用堆栈。

Because otherwise the continuation thread would lose context.

await 恢复时,它只需要足够的上下文来继续执行其 own 方法,并可能完成该方法。于是,就有了一个async状态机结构,装箱后放在堆上;此结构包含对局部变量的引用(包括 this 和方法参数)。但这就是程序正确性所必需的;不需要调用堆栈,因此不存储它。

您可以通过在 await 之后设置断点并观察调用堆栈来轻松地自己看到这一点。您会看到调用堆栈在第一个 await yield 之后消失了。或者 - 更恰当地说 - 调用堆栈表示继续 async 方法的代码,而不是 最初开始 async方法。

在实现层面,async/await 更像是回调而不是其他任何东西。当一个方法遇到 await 时,它会将其状态机结构粘在堆上(​​如果它还没有)并连接一个回调。该回调在任务完成时被触发(直接调用),并继续执行 async 方法。当该 async 方法完成时,它会完成 它的 任务,然后调用这些任务中的任何 await 以继续执行。因此,如果完成了整个任务序列,您实际上会得到一个调用堆栈,它是因果堆栈的倒置。

I would like to dump all stacktraces, even those not having its thread currently, so that I can find the deadlock.

所以,这里有几个问题。首先,没有所有 Task 对象(或更一般地,类任务对象)的全局列表。那将是一件很难得到的事情。

其次,对于每个异步 method/task,无论如何都没有因果链。编译器不会生成一个,因为它不是正确操作所必需的。

这并不是说这些问题中的任何一个都无法克服 - 只是很难。我用 AsyncDiagnostics library 对因果链问题做了一些工作。它在这一点上相当陈旧,但应该很容易升级到 .NET Core。它使用 PostSharp 修改编译器为每个方法生成的代码并手动跟踪因果链。

但是,AsyncDiagnotics 的目标是将因果关系链接到异常。获取所有类似任务的列表并将因果关系链与每个任务相关联是另一个问题,可能需要使用附加的分析器。我知道其他公司也想要这个解决方案,但是 none 他们已经投入了必要的时间来创建一个;他们都发现实施代码审查、审计和开发人员培训更有效。

我将 Stephen Cleary 的答案标记为正确答案。他给了提示,并深刻解释了为什么这么难。

我发布了这个替代答案来解释我们最终如何解决它以及我们决定做什么。


解决问题的变通方法

假设:堆栈跟踪包括自己的代码就足够了。

基于假设我们可以这样:

  1. 封装所有调用的外部异步方法(跟踪它们的进入和离开)
  2. 实施样式检查,这将警告在您的项目命名空间之外使用任何异步方法

广告 1.: 封装

假设一个外部方法Task ExternalObject.ExternalAsync()。我们将创建封装扩展方法:

public static async Task MyExternalAsync(this ExternalObject obj)
{
    using var disposable = AsyncStacktraces.MethodStarted();

    await obj.ExternalAsync();
}

AsyncStacktraces.MethodStarted(); 静态调用期间,当前堆栈跟踪将从 Environment.StackTrace 属性 与 disposable 对象一起记录到某个静态字典中。不会有性能问题,因为异步方法本身很可能比堆栈跟踪检索昂贵得多。

disposable 对象将实现 IDisposable 接口。 .Dispose() 方法将在 MyExternalAsync() 方法结束时从静态字典中删除当前堆栈跟踪。

通常在解决方案中实际上只调用几十个外部异步方法,因此工作量非常低。

广告 2.:样式检查

自定义样式检查扩展会在任何人直接使用外部异步方法时发出警告。 CI 可以是 set-up 这样有这个警告就不会通过。在少数需要直接外部异步方法的地方,我们将使用 #pragma warning disable.