此类情况非常简单,通过普通的调试即可定位问题点
此类问题较不易排查,不过还是有突破口滴
方法1:通过代码走查,看看开有线程的代码块中是否有耗时的操作,有就标识出来,通过调试来定位问题
方法2:通过监控线程的行为,来定位问题.监控方式:
阻塞情况与耗时线程类似,不过代码走查对项目的熟悉程度较高,最简单的方式就是通过vs中断程序找到阻塞点后,按F11激活调试
死锁问题排查也非常简单,排查方法如下:
方法1:对加有锁的地方进行代码走查,检查是否会有死锁的可能
方法2:对加有锁的地方进行堆栈监控并输出监控日志(需手动添加监视代码)
方法3:重构加锁方式,实现内部监控并输出监控日志(与前面提到的自定义监控工具(gitee)
类似)
方法4:通过vs中断程序找到持有锁的线程,按F11看能否成功激活调试,若激活不了程序继续运行,则大概率说明可能存在阻塞(如Thread.Sleep(Long Long Time)
)或者死锁(此方法最简单)
方法5:使用vs扩展PostSharp检测
方法6:使用WinDbg工具分析
所接手的项目为一个WPF项目其中使用了多线程,大多是使用Task.Run((=>{ ... }))
的方式来搞这个线程,当然也有原生的Thread
还有Task.Factory.StartNew()
的形式,排查多线程问题还是头一次接触,于是两眼发黑不晓得该怎么排查,不过关系不大,不是还有强大的搜索引擎么,于是看别人是怎么搞的,搜索到的有用的文章也是寥寥无几,估计是跟关键词的选取有关,其中搜到java的文章里面说道使用jdk自带的jconsole,Jstack来监控,泥马要是.NET里面有类似的这玩意儿就好了,也有通过WinDbg来分析dump文件来排查问题的,一看那GUI绝了,感觉垮了几个世纪,没办法只有硬着头皮试试看,不试不知道,一试吓一跳,果然没能分析出来
好在最后来了个灵感——你说可不可以通过某个方式来监控线程的行为喃?
带着这个问题便开始思考,没想到灵感炸现:.NET里面不是有delegate,Action,Func吗?于是想到通过注入监控器到线程中的形式来监控线程的行为,便开始撸代码呗,于是就有了上面的监控工具的产生,最终也是通过这个方式定位到问题点,项目的“前任”竟然在Task.Run
里面使用了轮询休眠的方式做定时任务,好家伙…找到问题点那就改呗
这里记录下关于Task.Run
,Parallel.ForEach
背后的原理,这两种形式背后使用了线程池的方式来工作的,既然是线程池,那它所持有的线程数量是有限的,如果这些线程长时间去处理那些耗时的操作比如长轮询+睡眠
的形式必然会出问题,导致其他地方不能即时的申请到线程池中的可用线程来处理数据,造成延时问题的产生,甚至还有死锁的风险,来看下面的这段代码:
internal class Program
{
private static void Main(string[] args)
{
for (int i = 1; i < 50; i++)
{
DoSomething(i);
}
Thread.Sleep(500);
DoOtherThing();
Thread.Sleep(-1);
}
public static void DoSomething(int flag)
{
Console.WriteLine($"{flag}申请线程:");
Task.Run(() =>
{
Console.WriteLine($"{flag}申请到线程,线程ID:{Thread.CurrentThread.ManagedThreadId}");
while (true)
{
Thread.Sleep(1000);
}
});
}
public static void DoOtherThing()
{
Console.WriteLine($"另一个线程开始申请线程,时间{DateTime.Now}");
Task.Run(() =>
{
Console.WriteLine($"另一个线程申请到线程,时间{DateTime.Now}");
while (true)
{
Task.Delay(10).Wait();
}
});
}
}
通过运行程序可以看到DoOtherThing()
方法想要开一个线程去处理事情,出现了极高的延迟,解决方法是如果线程处理事情特别耗时,那就不要用线程池的方式了,DoSomething(int flag)
方法改写如下
public static void DoSomething(int flag)
{
Console.WriteLine($"{flag}申请线程:");
Task.Factory.StartNew(() =>
{
Console.WriteLine($"{flag}申请到线程,线程ID:{Thread.CurrentThread.ManagedThreadId}");
while (true)
{
Thread.Sleep(1000);
}
},default(CancellationToken),TaskCreationOptions.LongRunning,TaskScheduler.Default);
}
或者
public static void DoSomething(int flag)
{
Console.WriteLine($"{flag}申请线程:");
new Task(() =>
{
Console.WriteLine($"{flag}申请到线程,线程ID:{Thread.CurrentThread.ManagedThreadId}");
while (true)
{
Thread.Sleep(1000);
}
}, TaskCreationOptions.LongRunning).Start();
}
参考:
.NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况 - walterlv - 博客园 (cnblogs.com)
记一次 .NET某汽车零件采集系统 卡死分析 - 一线码农 - 博客园 (cnblogs.com)
使用 Task.Wait()?立刻死锁(deadlock) - walterlv
不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 Lazy 中导致死锁 - walterlv
在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁 - walterlv
了解 .NET 的默认 TaskScheduler 和线程池(ThreadPool)设置,避免让 Task.Run 的性能急剧降低 - walterlv
How to debug .NET Deadlocks (C# Deadlocks in Depth - Part 3) | Michael’s Coding Spot (michaelscodingspot.com)
How to capture and debug .NET application crash dumps in Windows – 1.21 kilobytes (keithbabinec.com)