bh213
bh213

Reputation: 6519

How to get non-current thread's 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.

Upvotes: 35

Views: 20929

Answers (7)

Jeremy Thompson
Jeremy Thompson

Reputation: 65534

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

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

Upvotes: 0

Michael Burr
Michael Burr

Reputation: 340178

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:

Upvotes: 2

Andreas
Andreas

Reputation: 2075

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?

Upvotes: 13

Joe Albahari
Joe Albahari

Reputation: 30934

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).

Upvotes: 23

Andrew Rondeau
Andrew Rondeau

Reputation: 683

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

Upvotes: 5

Dirk Bonn&#233;
Dirk Bonn&#233;

Reputation: 648

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.

Upvotes: 18

Brian Rasmussen
Brian Rasmussen

Reputation: 116401

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

Upvotes: 12

Related Questions