如何获取非当前线程的堆栈跟踪?

34

可以使用 System.Diagnostics.StackTrace 获取堆栈跟踪信息,但需要暂停线程。挂起和恢复函数已经过时,因此我期望有更好的方式存在。

6个回答

22

注意:请跳到本答案底部查看更新。

以下是目前对我有效的方法:

StackTrace GetStackTrace (Thread targetThread)
{
    StackTrace stackTrace = null;
    var ready = new ManualResetEventSlim();

    new Thread (() =>
    {
        // Backstop to release thread in case of deadlock:
        ready.Set();
        Thread.Sleep (200);
        try { targetThread.Resume(); } catch { }
    }).Start();

    ready.Wait();
    targetThread.Suspend();
    try { stackTrace = new StackTrace (targetThread, true); }
    catch { /* Deadlock */ }
    finally
    {
        try { targetThread.Resume(); }
        catch { stackTrace = null;  /* Deadlock */  }
    }

    return stackTrace;
}
如果出现死锁,系统会自动释放死锁并返回一个空的跟踪记录(然后您可以再次调用它)。需要注意的是,在我的 Core i7 机器上进行了几天的测试后,只有一次出现了死锁。然而,在单核 VM 上,当 CPU 运行到 100% 时,死锁很常见。更新: 这种方法仅适用于 .NET Framework。在 .NET Core 和 .NET 5+ 中,无法调用 SuspendResume,因此必须使用替代方法,如 Microsoft 的 ClrMD 库。添加对 Microsoft.Diagnostics.Runtime 包的 NuGet 引用,然后可以调用 DataTarget.AttachToProcess 来获取关于线程和堆栈的信息。请注意,您无法对自己的进程进行采样,因此必须启动另一个进程,但这并不难。以下是一个基本的控制台演示,说明了该过程,并使用重定向的 stdout 将堆栈跟踪发送回主机:
using Microsoft.Diagnostics.Runtime;
using System.Diagnostics;
using System.Reflection;

if (args.Length == 3 &&
    int.TryParse (args [0], out int pid) &&
    int.TryParse (args [1], out int threadID) &&
    int.TryParse (args [2], out int sampleInterval))
{
    // We're being called from the Process.Start call below.
    ThreadSampler.Start (pid, threadID, sampleInterval);
}
else
{
    // Start ThreadSampler in another process, with 100ms sampling interval
    var startInfo = new ProcessStartInfo (
        Path.ChangeExtension (Assembly.GetExecutingAssembly().Location, ".exe"),
        Process.GetCurrentProcess().Id + " " + Thread.CurrentThread.ManagedThreadId + " 100")
    {
        RedirectStandardOutput = true,
        CreateNoWindow = true
    };

    var proc = Process.Start (startInfo);

    proc.OutputDataReceived += (sender, args) =>
        Console.WriteLine (args.Data != "" ? "  " + args.Data : "New stack trace:");

    proc.BeginOutputReadLine();

    // Do some work to test the stack trace sampling
    Demo.DemoStackTrace();

    // Kill the worker process when we're done.
    proc.Kill();
}

class Demo
{
    public static void DemoStackTrace()
    {
        for (int i = 0; i < 10; i++)
        {
            Method1();
            Method2();
            Method3();
        }
    }

    static void Method1()
    {
        Foo();
    }

    static void Method2()
    {
        Foo();
    }

    static void Method3()
    {
        Foo();
    }

    static void Foo() => Thread.Sleep (100);
}

static class ThreadSampler
{
    public static void Start (int pid, int threadID, int sampleInterval)
    {
        DataTarget target = DataTarget.AttachToProcess (pid, false);
        ClrRuntime runtime = target.ClrVersions [0].CreateRuntime();

        while (true)
        {
            // Flush cached data, otherwise we'll get old execution info.
            runtime.FlushCachedData();

            foreach (ClrThread thread in runtime.Threads)
                if (thread.ManagedThreadId == threadID)
                {
                    Console.WriteLine();   // Signal new stack trace

                    foreach (var frame in thread.EnumerateStackTrace().Take (100))
                        if (frame.Kind == ClrStackFrameKind.ManagedMethod)
                            Console.WriteLine ("    " + frame.ToString());

                    break;
                }

            Thread.Sleep (sampleInterval);
        }
    }
}

这是 LINQPad 6+ 用来展示查询实时执行跟踪的机制(增加了额外的检查、元数据探测和更复杂的进程间通信)。


4
仍存在微小的死锁风险:如果运行时在“ready.Wait()”和“targetThread.Suspend()”之间挂起主线程,则仍可能出现死锁,因为备用线程已经退出。我认为,您需要在解锁线程中设置循环,只有当主线程安全地发出退出函数的信号时,才会离开该循环。 - Andreas
4
Thread.Suspend()和Thread.Resume()在Framework中被标记为过时,因此任何使用Warnings As Errors的人都需要在方法之前使用#pragma warning disable 0618,并在之后使用#pragma warning restore 0618才能使这段代码编译通过。 - Warren Rumak
3
不幸的是,这种技术现在已经过时:https://msdn.microsoft.com/zh-cn/library/t2k35tat(v=vs.110).aspx - Andrew Rondeau
@AndrewRondeau 有其他的选择吗? - void.pointer
1
Joe:谢谢,这很酷。@void.pointer:我整理了一些关于如何解决这个问题的笔记。请注意,在我2017年工作的应用程序中,我们不会发布像Joe的解决方案那样的东西。请参阅https://andrewrondeau.herokuapp.com/how_to_get_a_stack_trace_of_a_background_thread_in_net - Andrew Rondeau
显示剩余5条评论

18

这是一篇旧帖子,但我想警告大家关于提出的解决方案:挂起和恢复方案是不可行的 - 我刚刚在我的代码中试图执行Suspend / StackTrace / Resume序列时遇到了死锁。

问题在于StackTrace构造函数执行了RuntimeMethodHandle -> MethodBase转换,导致更改了内部的MethodInfoCache,从而需要获取一个锁。死锁发生的原因是我正要审查的线程也在做反射,并持有该锁。

很遗憾,挂起/恢复操作没有在StackTrace构造函数内完成 - 如果是这样,这个问题就可以轻松解决。


1
完全正确 - 我在这样做时遇到了死锁。不过似乎有一种解决方法(请参见我的答案)。 - Joe Albahari

13

更新 2022-04-28: 此答案仅适用于 .NET Framework,不兼容 .NET Core 和 .NET Standard。由于我们都需要早日迁移,因此您不应再在新代码中使用它。

如我评论中所提到的,上面提出的解决方案仍然存在微小的死锁概率。请查看下面的我的版本。

private static StackTrace GetStackTrace(Thread targetThread) {
using (ManualResetEvent fallbackThreadReady = new ManualResetEvent(false), exitedSafely = new ManualResetEvent(false)) {
    Thread fallbackThread = new Thread(delegate() {
        fallbackThreadReady.Set();
        while (!exitedSafely.WaitOne(200)) {
            try {
                targetThread.Resume();
            } catch (Exception) {/*Whatever happens, do never stop to resume the target-thread regularly until the main-thread has exited safely.*/}
        }
    });
    fallbackThread.Name = "GetStackFallbackThread";
    try {
        fallbackThread.Start();
        fallbackThreadReady.WaitOne();
        //From here, you have about 200ms to get the stack-trace.
        targetThread.Suspend();
        StackTrace trace = null;
        try {
            trace = new StackTrace(targetThread, true);
        } catch (ThreadStateException) {
            //failed to get stack trace, since the fallback-thread resumed the thread
            //possible reasons:
            //1.) This thread was just too slow (not very likely)
            //2.) The deadlock ocurred and the fallbackThread rescued the situation.
            //In both cases just return null.
        }
        try {
            targetThread.Resume();
        } catch (ThreadStateException) {/*Thread is running again already*/}
        return trace;
    } finally {
        //Just signal the backup-thread to stop.
        exitedSafely.Set();
        //Join the thread to avoid disposing "exited safely" too early. And also make sure that no leftover threads are cluttering iis by accident.
        fallbackThread.Join();
    }
}
}

我认为ManualResetEventSlim中的“fallbackThreadReady”并不是真正必要的,但在这种微妙的情况下,为什么要冒险呢?


1
NB:ManualResetEventSlim 是 IDisposable - Mark Sowul
@MarkSowul:添加了一个 using 语句。谢谢你的提示。 - Andreas
你认为这种方法是死锁安全的吗?编辑:你提到了一个评论,但我不确定你是否指的是OP提供的上述解决方案的评论。 - Hatchling
2
@Hatchling:我没有看到死锁的任何可能性。虽然可能仍有一种可能性,但我从未在这段代码中遇到过死锁。 - Andreas

12

那就是我最终做的事情。 - bh213
1
要小心不要引入死锁。如果您在一个线程持有您需要的锁时挂起该线程,将会发生死锁。最常见的原因可能是线程共享一个流(例如写入控制台或类似情况)。 - Brian Rasmussen
2
这个现在已经被弃用了吗?有更好的方法来做这件事吗? - Caleb Seelhoff

5

2
我认为,如果你想在没有目标线程的合作下完成这个任务(例如通过让它调用一个方法,在信号量上阻塞它,而你的线程则执行堆栈跟踪),你需要使用已弃用的API。

一个可能的替代方案是使用.NET调试器使用的基于COM的ICorDebug接口。 MDbg代码库可能会给你一个开端:


1
不,COM不是一个选项。在.NET中,挂起/恢复比COM的东西更加干净利落... - bh213

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接