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

发布于 2024-07-08 15:04:33 字数 83 浏览 13 评论 0原文

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

It is possible to get stacktrace using System.Diagnostics.StackTrace, but thread has to be suspended. Suspend and Resume function are obsolete, so I expect that better way exists.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

顾挽 2024-07-15 15:04:33

注意:跳到这个答案的底部进行更新。

这是到目前为止对我有用的方法:

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% 时,单核虚拟机上死锁很常见。

更新:此方法仅适用于 .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)。

NB: Skip to the bottom of this answer for an update.

Here's what's worked for me so far:

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;
}

If it deadlocks, the deadlock is automatically freed and you get back a null trace. (You can then call it again.)

I should add that after a few days of testing, I've only once been able to create a deadlock on my Core i7 machine. Deadlocks are common, though, on single-core VM when the CPU runs at 100%.

Update: This approach works only for .NET Framework. In .NET Core and .NET 5+, Suspend and Resume cannot be called, so you must use an alternative approach such as Microsoft's ClrMD library. Add a NuGet reference to the Microsoft.Diagnostics.Runtime package; then you can call DataTarget.AttachToProcess to obtain information about threads and stacks. Note that you cannot sample your own process, so you must start another process, but that is not difficult. Here is a basic Console demo that illustrates the process, using a redirected stdout to send the stack traces back to the host:

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);
        }
    }
}

This is the mechanism that LINQPad 6+ uses to show live execution tracking in queries (with additional checks, metadata probing and a more elaborate IPC).

‖放下 2024-07-15 15:04:33

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

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

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

This is an old Thread, but just wanted to warn about the proposed solution: The Suspend and Resume solution does not work - I just experienced a deadlock in my code trying the sequence Suspend/StackTrace/Resume.

The Problem is that StackTrace constructor does RuntimeMethodHandle -> MethodBase conversions, and this changes a internal MethodInfoCache, which takes a lock. The deadlock occurred because the thread I was examining also was doing reflection, and was holding that lock.

It is a pity that the suspend/resume stuff is not done inside the StackTrace constructor -then this problem could easily have been circumvented.

萌︼了一个春 2024-07-15 15:04:33

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

Update 2022-04-28: This answer does only work in .NET Framework. It is not compatible with .NET Core and .NET Standard. Since we all need to migrate sooner or later, you should not use it in new code anymore.

As mentioned in my comment, the proposed solution above does still have a tiny probability for a deadlock. Please find my version below.

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();
    }
}
}

I think, the ManualResetEventSlim "fallbackThreadReady" is not really necessary, but why risk anything in this delicate case?

誰ツ都不明白 2024-07-15 15:04:33

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

According to C# 3.0 in a Nutshell, this is one of the few situations where it is okay to call Suspend/Resume.

回忆追雨的时光 2024-07-15 15:04:33

看起来这在过去是受支持的操作,但不幸的是,Microsoft 已废弃此操作:https://msdn.microsoft.com/en-us/library/t2k35tat(v=vs.110).aspx

It looks like this was a supported operation in the past, but unfortunately, Microsoft made this obsolete: https://msdn.microsoft.com/en-us/library/t2k35tat(v=vs.110).aspx

紫南 2024-07-15 15:04:33

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

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

I think that if you want to do this without the cooperation of the target thread (such as by having it call a method that blocks it on a Semaphore or something while your thread does the stacktrace) you'll need to use the deprecated APIs.

A possible alternative is the use the COM-based ICorDebug interface that the .NET debuggers use. The MDbg codebase might give you a start:

待天淡蓝洁白时 2024-07-15 15:04:33

这适用于 4.6 以上的完整 .Net 框架,并引用 NuGet 包:Microsoft.Diagnostics.Runtime,v2.0.226801,适用于 .Net 4.6。

private string GetThreadStackTrace(int managedThreadId)
{
    var result = new StringBuilder();
    try
    {    
        using (var target = DataTarget.CreateSnapshotAndAttach(Process.GetCurrentProcess().Id))
        {
            var runtime = target.ClrVersions.First().CreateRuntime();

            var threadNameLookup = new Dictionary<int, string>();
            foreach (var obj in runtime.Heap.EnumerateObjects())
            {
                if (!(obj.Type is null) && obj.Type.Name == "System.Threading.Thread")
                {
                    var threadId = obj.ReadField<int>("m_ManagedThreadId");
                    if (threadId == managedThreadId)
                    {
                        var threadName = obj.ReadStringField("_Name");
                        threadNameLookup[threadId] = threadName;
                        break;
                    }
                }
            }

            foreach (var thread in runtime.Threads)
            {
                if (thread.ManagedThreadId == managedThreadId)
                {
                    threadNameLookup.TryGetValue(thread.ManagedThreadId, out string threadName);
                    result.AppendLine($"ManagedThreadId: {thread.ManagedThreadId}, Name: {threadName}, OSThreadId: {thread.OSThreadId}, Thread: IsAlive: {thread.IsAlive}, IsBackground: {thread.IsBackground}");
                    foreach (var clrStackFrame in thread.EnumerateStackTrace())
                    {
                        result.AppendLine($"{clrStackFrame.Method}");
                    }

                    break;
                }
            }
        }
    }
    catch (Exception ex)
    {
        result.AppendLine($"Error getting thread stack trace: {ex.Message}");
    }

    return result.ToString();
}

您可以获得多个线程,我个人在线程中止之前使用此方法来了解原因:

在此处输入图像描述

REF:所有线程堆栈https://stackoverflow.com/a/24315960/495455

This works for Full .Net Framework above 4.6 with a reference to the NuGet Package: Microsoft.Diagnostics.Runtime, v2.0.226801 which works with .Net 4.6.

private string GetThreadStackTrace(int managedThreadId)
{
    var result = new StringBuilder();
    try
    {    
        using (var target = DataTarget.CreateSnapshotAndAttach(Process.GetCurrentProcess().Id))
        {
            var runtime = target.ClrVersions.First().CreateRuntime();

            var threadNameLookup = new Dictionary<int, string>();
            foreach (var obj in runtime.Heap.EnumerateObjects())
            {
                if (!(obj.Type is null) && obj.Type.Name == "System.Threading.Thread")
                {
                    var threadId = obj.ReadField<int>("m_ManagedThreadId");
                    if (threadId == managedThreadId)
                    {
                        var threadName = obj.ReadStringField("_Name");
                        threadNameLookup[threadId] = threadName;
                        break;
                    }
                }
            }

            foreach (var thread in runtime.Threads)
            {
                if (thread.ManagedThreadId == managedThreadId)
                {
                    threadNameLookup.TryGetValue(thread.ManagedThreadId, out string threadName);
                    result.AppendLine(
quot;ManagedThreadId: {thread.ManagedThreadId}, Name: {threadName}, OSThreadId: {thread.OSThreadId}, Thread: IsAlive: {thread.IsAlive}, IsBackground: {thread.IsBackground}");
                    foreach (var clrStackFrame in thread.EnumerateStackTrace())
                    {
                        result.AppendLine(
quot;{clrStackFrame.Method}");
                    }

                    break;
                }
            }
        }
    }
    catch (Exception ex)
    {
        result.AppendLine(
quot;Error getting thread stack trace: {ex.Message}");
    }

    return result.ToString();
}

You can get more than one thread, I personally use this before Threads are aborted to understand why:

enter image description here

REF: All threads stacks https://stackoverflow.com/a/24315960/495455

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文