Reputation: 6519
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
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:
REF: All threads stacks https://stackoverflow.com/a/24315960/495455
Upvotes: 0
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:
http://blogs.msdn.com/jmstall/archive/2005/11/07/views_on_cordbg_and_mdbg.aspx
http://geekswithblogs.net/johnsPerfBlog/archive/2008/10/13/mdbg-a-managed-wrapper-around-icordebug.aspx
Upvotes: 2
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
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
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
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
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