可以使用 System.Diagnostics.StackTrace 获取堆栈跟踪信息,但需要暂停线程。挂起和恢复函数已经过时,因此我期望有更好的方式存在。
可以使用 System.Diagnostics.StackTrace 获取堆栈跟踪信息,但需要暂停线程。挂起和恢复函数已经过时,因此我期望有更好的方式存在。
注意:请跳到本答案底部查看更新。
以下是目前对我有效的方法:
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 机器上进行了几天的测试后,只有一次出现了死锁。然而,在单核 VM 上,当 CPU 运行到 100% 时,死锁很常见。更新: 这种方法仅适用于 .NET Framework。在 .NET Core 和 .NET 5+ 中,无法调用 Suspend
和 Resume
,因此必须使用替代方法,如 Microsoft 的 ClrMD 库。添加对 Microsoft.Diagnostics.Runtime 包的 NuGet 引用,然后可以调用 DataTarget.AttachToProcess
来获取关于线程和堆栈的信息。请注意,您无法对自己的进程进行采样,因此必须启动另一个进程,但这并不难。以下是一个基本的控制台演示,说明了该过程,并使用重定向的 stdout 将堆栈跟踪发送回主机: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+ 用来展示查询实时执行跟踪的机制(增加了额外的检查、元数据探测和更复杂的进程间通信)。
这是一篇旧帖子,但我想警告大家关于提出的解决方案:挂起和恢复方案是不可行的 - 我刚刚在我的代码中试图执行Suspend / StackTrace / Resume序列时遇到了死锁。
问题在于StackTrace构造函数执行了RuntimeMethodHandle -> MethodBase转换,导致更改了内部的MethodInfoCache,从而需要获取一个锁。死锁发生的原因是我正要审查的线程也在做反射,并持有该锁。
很遗憾,挂起/恢复操作没有在StackTrace构造函数内完成 - 如果是这样,这个问题就可以轻松解决。
更新 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”并不是真正必要的,但在这种微妙的情况下,为什么要冒险呢?
看起来过去这是一个被支持的操作,但不幸的是,微软已经将其废弃: https://msdn.microsoft.com/en-us/library/t2k35tat(v=vs.110).aspx
一个可能的替代方案是使用.NET调试器使用的基于COM的ICorDebug接口。 MDbg代码库可能会给你一个开端:
#pragma warning disable 0618
,并在之后使用#pragma warning restore 0618
才能使这段代码编译通过。 - Warren Rumak