艾伟:.Net 下跟踪线程挂起和程序死循环

简介: .Net 下调试跟踪线程挂起和程序死循环  作者:Eaglet     .Net 下的程序调试相对C/C++要简单很多,少了那些令人头疼的指针越界的问题。不过当你的程序遇到如下问题时,依然非常棘手:     1. 进程异常终止。

.Net 下调试跟踪线程挂起和程序死循环

  作者:Eaglet

     .Net 下的程序调试相对C/C++要简单很多,少了那些令人头疼的指针越界的问题。不过当你的程序遇到如下问题时,依然非常棘手:

     1. 进程异常终止。解决方案见 .Net 下未捕获异常的处理

     2. 内存泄漏或者内存申请后程序始终没有释放。解决方案见 用 .NET Memory Profiler 跟踪.net 应用内存使用情况--基本应用篇 。如果通过自己编写的程序监控,我将在以后的文章中阐述。

     3. 线程因未知原因挂起,比如死锁。

     4. 程序死循环。

     本文将阐述如果编写程序对后两者故障实时跟踪并报告。

  •      首先我们需要一个单独的监控线程来监控需要监控的线程

     我做了一个监控类 ThreadMonitor,在开始监控之前,我们将监控线程的优先级设置为最高。

         public  ThreadMonitor()
        
{
            _MonitorThread 
= new Thread(new ThreadStart(MonitorTask));
            _MonitorThread.Priority 
= ThreadPriority.Highest;
            _MonitorThread.IsBackground 
= true;

        }
  •      接下来我们为这个线程提供几个公共方法

Start  方法让调用者启动监控

Register 方法用于将需要监控的线程注册到监控列表中

Heartbeat 方法后面说明

         /**/ /// 
        
/// Start monitor
        
/// 

         public   void  Start()
        
{
            _MonitorThread.Start();
        }



        
/**/ /// 
        
/// Monitor register
        
/// 
        
/// Monitor parameter

         public   void  Register(MonitorParameter monitorPara)
        
{
            Debug.Assert(monitorPara 
!= null);
            Debug.Assert(monitorPara.Thread 
!= null);

            
if (GetTCB(monitorPara.Thread) != null)
            
{
                
throw new System.ArgumentException("Register repeatedly!");
            }


            
lock (_RegisterLock)
            
{
                _TCBTable.Add(monitorPara.Thread.ManagedThreadId, 
new TCB(monitorPara));
            }

        }


        
public   void  Heartbeat(Thread t)
        
{
            TCB tcb 
= GetTCB(t);
            
if (tcb == null)
            
{
                
throw new System.ArgumentException("This thread was not registered!");
            }


            tcb.LastHeartbeat 
= DateTime.Now;
            tcb.HitTimes 
= 0;
            tcb.Status 
&= ~ThreadStatus.Hang;
        }

 

下面让我来说说如何监控某个线程挂起。

监控线程提供了一个心跳调用 Heartbeat ,被监控的线程必须设置一个定时器定时向监控线程发送心跳,如果监控线程在一定时间内无法收到这个心跳消息,则认为被监控线程非正常挂起了。这个时间又MonitorParameter参数的HangTimeout指定。

光监控到线程挂起还不够,我们必须要报告线程当前挂起的位置才有实际意义。那么如何获得线程当前的调用位置呢?.Net framework 为我们提供了获取线程当前堆栈调用回溯的方法。见下面代码

 

         private   string  GetThreadStackTrace(Thread t)
        
{
            
bool needFileInfo = NeedFileInfo;

            t.Suspend();
            StackTrace stack 
= new StackTrace(t, needFileInfo);
            t.Resume();

            
return stack.ToString();
        }

 

这里需要说明的是StackTrace(t, needFileInfo) 必须在线程t Suspend后 才能调用,否则会发生异常。但Thread.Suspend 调用是比较危险的,因为调用者无法知道线程t挂起前的运行状况,可能线程t目前正在等待某个资源,这时强制挂起,非常容易造成程序死锁。不过值得庆幸的是StackTrace(t, needFileInfo)的调用不会和其他线程尤其是调用线程产生资源冲突,但我们必须在这一句执行结束后迅速调用 t.Resume 结束线程t的挂起状态。

谈完了对线程非正常挂起的监控,再谈谈对程序死循环的监控。

在决定采用我现在的这个方案之前,我曾经想通过 GetThreadTimes 这个API 函数得到被监控线程的实际CPU运行时间,通过这个时间来计算其CPU占有率,但很遗憾,我的尝试失败了。通过非当前线程下调用 GetThreadTimes 无法得到对应线程的CPU时间。(好像非托管线程可以,但.Net的托管线程我试了,确实不行,但原因我还没弄明白)另外GetThreadTimes 统计不够准确 见 对老赵写的简单性能计数器的修改续- 关于 GetThreadTimes

所以没有办法,我采用了一个不是很理想的方案

定时统计当前进程的TotalProcessorTime 来计算当前线程的CPU占有率,如果这个CPU占有率在一段时间内大于 100 / (CPU 数)* 90% ,则认为当前进程出现了死循环。这个测试时间由  MonitorParameter参数的DeadCycleTimeout 属性指定。

这就出现了一个问题,我们只知道程序死循环了,但不知道具体是那个线程死循环,那么如何找到真正死循环的线程呢?

我采用的方法是每秒钟检测一次线程当前状态,如果当前状态为运行状态则表示命中一次,在确认出现死循环后我们在来检查在一个检查周期内的命中次数,如果这个命中次数足够高,则认为是该线程死循环了。不过这样还是有问题,主线程在等待windows 消息时 或者控制台程序线程在等待控制台输入时,该线程的状态居然始终是 Runing ,其实是阻塞了,但我没有找到一个很好的方法来得到线程当前处于阻塞状态。怎么办?我想了个笨办法,就是在上面两个条件都符合的情况下再看看在此期间有没有心跳,如果没有心跳,说明死循环了。但如果有心跳也不一定就没有死循环,遇到这种情况,就将可疑的都全部报告了,靠人来判断吧。

我写了一个示例代码,代码中有一个Winform 主线程 和 一个计数器线程,计数器线程每秒记一次数,并更新界面。监控线程检查到非正常挂起或者死循环,将在当前目录下写一个Report.log 输出监控报告。

点击Hang后主线程休眠20秒,计数器线程由于要更新界面,也同样会被挂起。

监控线程检查到两个线程挂起后报告如下:

2:38:40 PM
ThreadMonitorEvent
Thread Name:Main thread
Thread Status:Hang
Thread Stack:   at System.Threading.Thread.SleepInternal(Int32 millisecondsTimeout)
   at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
   at DotNetDebug.Form1.buttonHang_Click(Object sender, EventArgs e)
   at System.Windows.Forms.Control.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ButtonBase.WndProc(Message& m)
   at System.Windows.Forms.Button.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
   at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
   at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.Run(Form mainForm)
   at DotNetDebug.Program.Main()
   at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()


2:38:40 PM
ThreadMonitorEvent
Thread Name:Counter thread
Thread Status:Hang
Thread Stack:   at System.Threading.WaitHandle.WaitOneNative(SafeWaitHandle waitHandle, UInt32 millisecondsTimeout, Boolean hasThreadAffinity, Boolean exitContext)
   at System.Threading.WaitHandle.WaitOne(Int64 timeout, Boolean exitContext)
   at System.Threading.WaitHandle.WaitOne(Int32 millisecondsTimeout, Boolean exitContext)
   at System.Windows.Forms.Control.WaitForWaitHandle(WaitHandle waitHandle)
   at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
   at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
   at System.Windows.Forms.Control.Invoke(Delegate method)
   at DotNetDebug.Form1.Counter()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

点击DeadCycle 按钮后,让计数器线程死循环,但主线程不死循环。

监控线程检查到计数器线程死循环后报告如下:

 

2:37:51 PM
ThreadMonitorEvent
Thread Name:Counter thread
Thread Status:Hang
Thread Stack:   at DotNetDebug.Form1.DoDeadCycle()
   at DotNetDebug.Form1.Counter()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()


2:37:52 PM
ThreadMonitorEvent
Thread Name:Counter thread
Thread Status:Hang, DeadCycle
Thread Stack:   at DotNetDebug.Form1.DoDeadCycle()
   at DotNetDebug.Form1.Counter()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

 

下面是示例代码在

以下是测试代码。完整源码的下载位置:  完整源码  

 

using  System;
using  System.Collections.Generic;
using  System.ComponentModel;
using  System.Data;
using  System.Drawing;
using  System.Text;
using  System.Windows.Forms;
using  System.Threading;
using  Sys.Diagnostics;

namespace  DotNetDebug
{
    
public partial class Form1 : Form
    
{
        Thread _CounterThread;
        ThreadMonitor _ThreadMonitor 
= new ThreadMonitor();
        
bool _DeadCycle = false;

        
delegate void CounterDelegate();

        
private void DoDeadCycle()
        
{
            
while (_DeadCycle)
            
{
            }

        }


        
private void Counter()
        
{
            
int count = 0;
            
while (true)
            
{
                DoDeadCycle();
                labelCounter.Invoke(
new CounterDelegate(delegate() { labelCounter.Text = (count++).ToString(); }));
                _ThreadMonitor.Heartbeat(Thread.CurrentThread);

                Thread.Sleep(
1000);
            }

        }


        
public Form1()
        
{
            InitializeComponent();
        }


        
void OnThreadMonitorEvent(object sender, ThreadMonitor.ThreadMonitorEvent args)
        
{
            StringBuilder sb 
= new StringBuilder();

            sb.AppendLine(DateTime.Now.ToLongTimeString());
            sb.AppendLine(
"ThreadMonitorEvent");
            sb.AppendLine(
"Thread Name:" + args.Name);
            sb.AppendLine(
"Thread Status:" + args.Status.ToString());
            sb.AppendLine(
"Thread Stack:" + args.StackTrace);

            
using (System.IO.FileStream fs =
                
new System.IO.FileStream("report.log", System.IO.FileMode.Append, 
                System.IO.FileAccess.Write))
            
{
                
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fs))
                
{
                    sw.WriteLine(sb.ToString());
                }

            }

        }



        
private void Form1_Load(object sender, EventArgs e)
        
{
            _ThreadMonitor.ThradMonitorEventHandler 
+=
                
new EventHandler<ThreadMonitor.ThreadMonitorEvent>(OnThreadMonitorEvent);

            _CounterThread 
= new Thread(new ThreadStart(Counter));
            _CounterThread.IsBackground 
= true;


            _ThreadMonitor.Register(
new ThreadMonitor.MonitorParameter(
                Thread.CurrentThread, 
"Main thread"100005000,
                ThreadMonitor.MonitorFlag.MonitorHang 
|
                ThreadMonitor.MonitorFlag.MonitorDeadCycle));

            _ThreadMonitor.Register(
new ThreadMonitor.MonitorParameter(
                _CounterThread, 
"Counter thread",
                ThreadMonitor.MonitorFlag.MonitorHang 
|
                ThreadMonitor.MonitorFlag.MonitorDeadCycle));

            _CounterThread.Start();

            timerHeartbeat.Interval 
= 1000;
            timerHeartbeat.Enabled 
= true;

            _ThreadMonitor.Start();
        }


        
private void timerHeartBeat_Tick(object sender, EventArgs e)
        
{
            _ThreadMonitor.Heartbeat(Thread.CurrentThread);
        }


        
private void ButtonDeadCycle_Click(object sender, EventArgs e)
        
{
            _DeadCycle 
= true;
        }


        
private void buttonHang_Click(object sender, EventArgs e)
        
{
            Thread.Sleep(
20000);
        }

    }

}

 

 

 

 

目录
相关文章
|
1天前
|
Ubuntu 持续交付 API
如何使用 dotnet pack 打包 .NET 跨平台程序集?
`dotnet pack` 是 .NET Core 的 NuGet 包打包工具,用于将代码打包成 NuGet 包。通过命令 `dotnet pack` 可生成 `.nupkg` 文件。使用 `--include-symbols` 和 `--include-source` 选项可分别创建包含调试符号和源文件的包。默认情况下,`dotnet pack` 会先构建项目,可通过 `--no-build` 跳过构建。此外,还可以使用 `--output` 指定输出目录、`-c` 设置配置等。示例展示了创建类库项目并打包的过程。更多详情及命令选项,请参考官方文档。
29 11
|
8天前
|
Rust 并行计算 安全
揭秘Rust并发奇技!线程与消息传递背后的秘密,让程序性能飙升的终极奥义!
【8月更文挑战第31天】Rust 以其安全性和高性能著称,其并发模型在现代软件开发中至关重要。通过 `std::thread` 模块,Rust 支持高效的线程管理和数据共享,同时确保内存和线程安全。本文探讨 Rust 的线程与消息传递机制,并通过示例代码展示其应用。例如,使用 `Mutex` 实现线程同步,通过通道(channel)实现线程间安全通信。Rust 的并发模型结合了线程和消息传递的优势,确保了高效且安全的并行执行,适用于高性能和高并发场景。
22 0
|
7天前
|
开发框架 Android开发 iOS开发
跨平台开发的双重奏:Xamarin在不同规模项目中的实战表现与成功故事解析
【8月更文挑战第31天】在移动应用开发领域,选择合适的开发框架至关重要。Xamarin作为一款基于.NET的跨平台解决方案,凭借其独特的代码共享和快速迭代能力,赢得了广泛青睐。本文通过两个案例对比展示Xamarin的优势:一是初创公司利用Xamarin.Forms快速开发出适用于Android和iOS的应用;二是大型企业借助Xamarin实现高性能的原生应用体验及稳定的后端支持。无论是资源有限的小型企业还是需求复杂的大公司,Xamarin均能提供高效灵活的解决方案,彰显其在跨平台开发领域的强大实力。
15 0
|
16天前
|
Java 调度
|
2月前
|
Linux C# iOS开发
如何用 WinDbg 调试Linux上的 .NET程序
【7月更文挑战第13天】 1. `dotnet-dump`: Collects process dumps with `dotnet-dump collect -p &lt;process_id&gt;`. 2. `lldb`: Debugs Mono runtime apps on macOS/Linux. 3. **Visual Studio Code**: Remotely debugs .NET via the C# extension. 4. **JetBrains Rider**: Supports remote debugging of .NET on Linux.
|
20天前
|
安全 C# 开发者
【C# 多线程编程陷阱揭秘】:小心!那些让你的程序瞬间崩溃的多线程数据同步异常问题,看完这篇你就能轻松应对!
【8月更文挑战第18天】多线程编程对现代软件开发至关重要,特别是在追求高性能和响应性方面。然而,它也带来了数据同步异常等挑战。本文通过一个简单的计数器示例展示了当多个线程无序地访问共享资源时可能出现的问题,并介绍了如何使用 `lock` 语句来确保线程安全。此外,还提到了其他同步工具如 `Monitor` 和 `Semaphore`,帮助开发者实现更高效的数据同步策略,以达到既保证数据一致性又维持良好性能的目标。
24 0
|
23天前
|
开发框架 NoSQL .NET
使用 Asp.net core webapi 集成配置系统,提高程序的灵活和可维护性
使用 Asp.net core webapi 集成配置系统,提高程序的灵活和可维护性
|
23天前
|
消息中间件 监控 Java
最佳实践|一文讲解端线程死循环的治理
本文旨在介绍钉钉 Android 团队死循环检测工具建设的思路和典型案例的修复历程。希望通过此次分享,对同样面临类似死循环问题的团队能够有所启发。
|
27天前
|
安全 Java API
揭秘Java并发编程的神秘面纱:线程安全与性能优化之间的微妙舞蹈,如何让你的程序在多核时代中翱翔!
【8月更文挑战第12天】随着多核处理器的普及,Java并发编程越发重要。线程安全确保多线程环境下的程序一致性,而性能优化则让程序高效运行。通过同步机制如`synchronized`关键字或`ReentrantLock`接口,我们可以实现线程安全,如在银行账户存款操作中限制并发访问。然而,过度同步会导致性能下降,因此采用细粒度锁和利用Java并发工具类(如`ConcurrentHashMap`)可提高程序的并发能力。理解这些概念并加以实践,是每个Java开发者提升技能的关键。
29 0