32

可以使用 System.Diagnostics.StackTrace 获取堆栈跟踪,但必须暂停线程。Suspend 和 Resume 功能已过时,因此我希望存在更好的方法。

4

6 回答 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 机器上创建一个死锁。但是,当 CPU 以 100% 运行时,死锁在单核 VM 上很常见。

更新:此方法仅适用于 .NET Framework。在 .NET Core 和 .NET 5+ 中,Suspend并且Resume无法调用,因此您必须使用替代方法,例如 Microsoft 的 ClrMD 库。添加对 Microsoft.Diagnostics.Runtime 包的 NuGet 引用;然后您可以调用DataTarget.AttachToProcess以获取有关线程和堆栈的信息。请注意,您不能对自己的流程进行抽样,因此您必须启动另一个流程,但这并不困难。这是一个基本的控制台演示,说明了该过程,使用重定向的标准输出将堆栈跟踪发送回主机:

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+ 用来在查询中显示实时执行跟踪的机制(带有额外的检查、元数据探测和更精细的 IPC)。

于 2012-03-07T04:09:06.173 回答
18

这是一个旧线程,但只是想警告建议的解决方案:暂停和恢复解决方案不起作用 - 我刚刚在尝试序列暂停/堆栈跟踪/恢复的代码中遇到了死锁。

问题是 StackTrace 构造函数执行 RuntimeMethodHandle -> MethodBase 转换,这改变了内部 MethodInfoCache,它需要一个锁。发生死锁是因为我正在检查的线程也在进行反射,并且持有该锁。

遗憾的是,挂起/恢复的东西没有在 StackTrace 构造函数中完成——那么这个问题很容易被规避。

于 2010-08-30T17:47:53.310 回答
12

根据C# 3.0 in a Nutshell,这是可以调用 Suspend/Resume 的少数情况之一。

于 2009-02-09T12:59:02.667 回答
12

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

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”并不是真正必要的,但为什么要在这种微妙的情况下冒险呢?

于 2013-02-18T11:36:53.723 回答
5

看起来这是过去受支持的操作,但不幸的是,微软已经过时了:https ://msdn.microsoft.com/en-us/library/t2k35tat(v=vs.110).aspx

于 2017-04-24T14:45:12.043 回答
2

我认为,如果您想在没有目标线程的合作的情况下执行此操作(例如,在您的线程执行堆栈跟踪时,让它调用一个在信号量上阻塞它的方法或其他东西),您将需要使用已弃用的 API。

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

于 2008-11-12T20:50:09.683 回答