Spis treści
- Hello world, które się nie kończy
- Jak namierzyć wątki uruchomione jako "nie w tle"
- Brak korelacji 1:1 dla Sys_ThID i Mgt_ThID
- Co jeśli mamy wskaźnik na instancję Thread?
- A może WinDbg?
- ClrMD
- Ciekawostka - XSharper.Core
Hello world, które się nie kończy
Czasami jest tak, że aplikacja powinna się zamknąć gdy kończymy działanie głównego wątku ale tak się nie dzieje. Choćby ta poniższa:
static void Main(string[] args)
{
CiekawaFunkcja();
Console.WriteLine("Hello world. Koniec.");
}
"Problemem" jest CiekawaFunkcja()
, której zawartość może wyglądać następująco:
static void CiekawaFunkcja()
{
AutoResetEvent evt = new AutoResetEvent(false);
Thread th = new Thread(state =>
{
evt.WaitOne();
});
th.IsBackground = false;
th.Start();
}
Znajduje się tam parametr IsBackground = false
, który powoduje, że proces nie może zostać automatycznie zamknięty. Proces nie zostanie zamknięty dopóki zawiera aktywne wątki nie w tle. Zgodnie z dokumentacją MSDN wątek startuje domyślnie jako:
- Nie w tle - gdy jest to główny wątek aplikacji
- Nie w tle - gdy użyto konstruktora
Thread()
- W tle - gdy wątek pochodzi z puli wątków (
ThreadPool
) - W tle - wątki powołane z
async/await
bo pochodzą z puli wątków - W tle - wątki nie zarządzalne/natywne, które są używane w kodzie zarządzalnym
Dlatego należy zawsze pamiętać aby tworząc nowy wątek ustawiać pole IsBackground = true
!
Namierzenie takiej sytuacji może okazać się nietrywialne bo przeszukanie całego projektu pod względem wystąpienia słowa "IsBackground" lub "Thread" nie zwróci żadnych wyników. Nie znaczy to jednak, że któraś z użytych bibliotek tego nie robi.
Jak namierzyć wątki uruchomione jako "nie w tle"
Sprawa jest nietrywialna gdyż w .NET nie ma czegoś takiego jak wylistowanie wszystkich wątków zarządzalnych... Jedyne co istnieje to lista wątków natywnych:
Process proc = Process.GetCurrentProcess();
foreach (ProcessThread thProc in proc.Threads
.OfType<ProcessThread>()
.OrderBy(o => o.Id)
)
{
// Brak pola thProc.ManagedThreadId
Console.WriteLine($"Sys_ThID = {thProc.Id}");
}
Wynik w/w kodu zobrazowany jest poniżej. Lista ID wątków (Sys_ThID) pokrywa się z tym co pokazuje ProcessExplorer ale te liczby ni jak mają się do ID wątku zarządzalnego (Mgt_ThID), który został zwrócony w aplikacji za pomocą metody Thread.CurrentThread.ManagedThreadId
dla głównego wątku ("Main") oraz drugiego stworzonego w kodzie ("Nowy").
Brak korelacji 1:1 dla Sys_ThID i Mgt_ThID
Sprawa jest o tyle nieciekawa, że nie można dokonać mapowania 1:1 ID wątku natywnego i zarządzalnego. Wynika to ze specyfiki .NET, gdzie jeden wątek natywny może zawierać kilka wątków zarządzalnych. Dokumentacja MSDN Managed and Unmanaged Threading in Windows mówi w tym temacie tyle (eng):
An operating-system ThreadId has no fixed relationship to a managed thread, because an unmanaged host can control the relationship between managed and unmanaged threads. Specifically, a sophisticated host can use the Fiber API to schedule many managed threads against the same operating system thread, or to move a managed thread among different operating system threads.
Jedyne co można zrobić to z wnętrza wątku zarządzalnego pobrać ID wątku natywnego w importując metodę natywną
[DllImport("Kernel32", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
public static extern Int32 GetCurrentWin32ThreadId();
Co jeśli mamy wskaźnik na instancję Thread?
Nawet jeśli w jakiś sposób mamy wskaźnik na wątek nie w tle to, żeby pobrać stos wywołać - co jest aktualnie wykonywane - należy użyć metod, które w przyszłości mogą nie być dostępne a aktualnie oznaczone są do usunięcia.
Poniższy kod listuje stos wywołań dla powołanego wątku, który stoi na metodzie AutoResetEvent.WaitOne()
.
// Thread.Suspend has been deprecated.
// Please use other classes in System.Threading, such as Monitor, Mutex, ...
_th.Suspend();
// This constructor has been deprecated.
// Please use a constructor that does not require a Thread parameter.
StackTrace trace = new StackTrace(_th, true);
Console.WriteLine($"\nStack dla Mgt_ThID = {_th.ManagedThreadId}:");
foreach (StackFrame sframe in trace.GetFrames())
{
Console.WriteLine(" " + sframe.GetMethod());
}
// Thread.Resume has been deprecated.
_th.Resume();
Wynik działania:
Stack dla Mgt_ThID = 3:
Int32 WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
Boolean InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
Boolean WaitOne(Int32, Boolean)
Boolean WaitOne()
Void <StartNonBackgroundThread>b__4_0(System.Object)
Void ThreadStart_Context(System.Object)
Void RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
Void Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
Void Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
Void ThreadStart(System.Object)
A może WinDbg?
Jeśli problem można można odtworzyć lokalnie na stacji jeszcze podczas tworzenia kodu nie potrzeba korzystać z niewygodnych w użyciu narzędzi takich jak WinDbg.
Przy WinDbg należy uważać aby aplikacja wykonująca zrzut pamięci działała w tym samym trybie (x86 lub x64) co aplikacja, której zrzut jest wykonywany. Do tego jeszcze trzeba pamiętać aby mieć bibliotekę mscordacwks.dll
, która pochodzi z maszyny na której występują problemy. Dodatkowo jeśli takiego debugowania nie robi się często to skonfigurowanie narzędzi i ich użycie to koszmar.
ClrMD
Z pomocą przychodzi narzędzie Microsoft.Diagnostics.Runtime w skrócie zwane ClrMD. Dzięki niemu w prosty sposób można pobrać listę wszystkich zarządzalnych wątków działającej aplikacji wraz z aktualnym stosem wywołań.
// ClrMD w wersji 0.9.170809.03
int pid = Process.GetProcessesByName("cmdWorker").First().Id;
using (DataTarget target = DataTarget.AttachToProcess(pid, 1000, AttachFlag.Passive))
{
ClrRuntime runtime = target.ClrVersions
.First()
.CreateRuntime();
foreach (ClrThread clrThread in runtime.Threads
.Where(w => false == w.IsBackground)
)
{
Console.WriteLine($"\nClrMd stack Mgt_ThID = {clrThread.ManagedThreadId}, " +
$"Sys_ThID = {clrThread.OSThreadId}:");
foreach (ClrStackFrame frame in clrThread.StackTrace
.Where(w => null != w.Method)
)
{
Console.WriteLine(" " + frame.Method.Name);
}
}
}
Wynik działania:
ClrMd stack Mgt_ThID = 1, Sys_ThID = 7208:
IL_STUB_PInvoke
ReadFileNative
Read
ReadBuffer
ReadLine
ReadLine
ReadLine
Main
ClrMd stack Mgt_ThID = 3, Sys_ThID = 9388:
WaitOneNative
InternalWaitOne
WaitOne
WaitOne
<StartNonBackgroundThread>b__4_0
ThreadStart_Context
RunInternal
Run
Run
ThreadStart
UWAGA: Najlepiej korzystać z biblioteki w zewnętrznej aplikacji bo w przeciwnym wypadku stos wywołań dla wątku w którym biblioteka została uruchomiona będzie pusty. Jeśli jednak nie ma wyjścia to należy pamiętać o fladze AttachFlag.Passive
, która nie spowoduje zawieszenia procesu.
Ciekawostka - XSharper.Core
Będąc już w temacie analizy niedziałającego poprawnie kodu czasami należy dowiedzieć się co dokładnie kryje się w obiektach używanych z bibliotek zewnętrznych (w tym dotnetowych, np. z GACa, stworzonych przez Microsoft)
Do takich celów dość dobrze sprawdza się biblioteka XSharper.Core.dll dzięki której można zrzucić zawartość całego obiektu do pliku a potem przejrzeć wartości pól w nim zawartych.
string dumped = Dump.ToDump(
obj,
new DumpSettings {
DisplayPrivate = true,
MaxDepth = 1000
});
File.WriteAllText(pathToFile, dumped);
Przykładowy wynik działania:
(CityClass) { /* #1, 001a0e24 */
_city = (string) "Łódź"
_nfo = (InfoClass) { /* #2, 003e799b */
_area = (Single) [293,25]
_population = (int) 693797 (0xa9625)
}
}