【问题标题】:How to get non-current thread's stacktrace?如何获取非当前线程的堆栈跟踪?
【发布时间】:2010-09-22 01:14:26
【问题描述】:

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

【问题讨论】:

    标签: .net multithreading debugging stack-trace visual-studio-debugging


    【解决方案1】:

    注意:跳到此答案的底部以获取更新。

    到目前为止,这对我有用:

    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+ 中,无法调用 SuspendResume,因此您必须使用替代方法,例如 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)。

    【讨论】:

    • 您可能希望使用第二个 ManualResetEvent 来避免 targetThread.Resume() 被执行并每次抛出异常... if (!noDeadLockSafeGuard.WaitOne( 200)) { 尝试 { targetThread.Resume(); } 捕捉 { } }
    • 仍然存在很小的死锁风险:如果运行时决定在“ready.Wait()”和“targetThread.Suspend()”之间暂停主线程,你可能仍然有由于 fallback-Thread 已经退出而出现死锁。 IMO 你需要在解锁线程中有一个循环,只有当主线程发出信号表明它安全退出函数时才会离开。
    • Thread.Suspend() 和 Thread.Resume() 在框架中被标记为过时,因此任何使用警告作为错误的人都需要在方法之前使用#pragma warning disable 0618,之后使用#pragma warning restore 0618让这段代码编译。
    • 不幸的是,这种技术现在已经过时了:msdn.microsoft.com/en-us/library/t2k35tat(v=vs.110).aspx
    • @AndrewRondeau 还有其他选择吗?
    【解决方案2】:

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

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

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

    【讨论】:

    • 完全正确 - 我在这样做时遇到了死锁。不过,似乎确实有一种解决方法(请参阅我的回答)。
    【解决方案3】:

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

    【讨论】:

    • 这就是我最终所做的。
    • 小心不要引入僵硬的死锁。如果您在线程持有您需要的锁时挂起它,您将遇到死锁。最常见的原因可能是线程共享流(例如写入控制台或类似内容)。
    • 现在已经弃用了吗?有没有更好的方法来做到这一点?
    【解决方案4】:

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

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

    【讨论】:

    • 注意:ManualResetEventSlim 是 IDisposable
    • @MarkSowul:添加了 using 语句。谢谢你的提示。
    • 你会说这种方法是死锁证明吗?编辑:您提到了评论,但我不确定您是否指的是对 OP 提供的上述解决方案的评论。
    • @Hatchling:我看不到任何死锁的可能性。可能仍然有可能,但我从来没有使用此代码。
    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

      • 不,COM 不是一个选项。 Suspend/Resume 感觉比 .NET 中的 COM 更干净...
      猜你喜欢
      • 1970-01-01
      • 2010-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-11
      • 2014-02-04
      相关资源
      最近更新 更多