System.Diagnostics.StackTrace を使用してスタックトレースを取得することは可能ですが、スレッドを中断する必要があります。Suspend と Resume 機能は廃止されたので、もっと良い方法があることを期待します。
6 に答える
注意:更新については、この回答の最後までスキップしてください。
これまでのところ、私のために働いたものは次のとおりです。
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;
}
デッドロックが発生した場合、デッドロックは自動的に解放され、null トレースが返されます。(その後、再度呼び出すことができます。)
数日間のテストの後、Core i7 マシンでデッドロックを作成できたのは 1 回だけです。ただし、CPU が 100% で実行されている場合、シングルコア VM ではデッドロックが一般的です。
更新: このアプローチは、.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+ がクエリでライブ実行追跡を表示するために使用するメカニズムです (追加のチェック、メタデータ プローブ、およびより精巧な IPC を使用)。
これは古いスレッドですが、提案された解決策について警告したいだけです: サスペンドと再開のソリューションは機能しません - Suspend/StackTrace/Resume のシーケンスを試みているコードでデッドロックが発生しました。
問題は、StackTrace コンストラクターが RuntimeMethodHandle -> MethodBase 変換を行うことです。これにより、ロックを取得する内部 MethodInfoCache が変更されます。私が調べていたスレッドもリフレクションを行っていて、そのロックを保持していたため、デッドロックが発生しました。
一時停止/再開が StackTrace コンストラクター内で行われないのは残念です。この問題は簡単に回避できたはずです。
私のコメントで述べたように、上記の提案されたソリューションには、デッドロックの可能性がまだわずかしかありません。以下の私のバージョンを見つけてください。
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"は実際には必要ないと思いますが、このデリケートなケースで何かを危険にさらすのはなぜですか?
C# 3.0 in a Nutshellによると、これは Suspend/Resume を呼び出しても問題ない数少ない状況の 1 つです。
ターゲット スレッドの協力なしでこれを行う場合 (スレッドがスタック トレースを実行している間に、セマフォなどでブロックするメソッドを呼び出すなど) は、非推奨の API を使用する必要があると思います。
考えられる代替手段は、.NET デバッガーが使用するCOM ベースの ICorDebugインターフェイスを使用することです。MDbg コードベースが手がかりになるかもしれません。