WPF记一次多线程中死锁以及加载页迟迟没能加载出数据问题

一、解决思路如下:

  1. 分析出现此类情况可能的原因,并一一列出来
  2. 根据列出的原因逐个分析,由简单到复杂
  3. 针对每种原因制定合理的排查方案
  4. 定位问题并修复BUG

二、多线程情况下加载页迟迟加载不出数据可能的原因:

  • 数据源缺失,即没有数据可加载
  • 出现了耗时任务,线程长时间被占用,无法让出资源
  • 出现了阻塞情况
  • 出现了死锁情况

三、制定排查方案和解决方案

1.数据源缺失

此类情况非常简单,通过普通的调试即可定位问题点

2.耗时线程问题

此类问题较不易排查,不过还是有突破口滴
方法1:通过代码走查,看看开有线程的代码块中是否有耗时的操作,有就标识出来,通过调试来定位问题
方法2:通过监控线程的行为,来定位问题.监控方式:

  • 自定义监控工具(gitee)
  • 使用vs中断程序功能,定位当前正在运行的线程
  • vs中的断点输出日志
  • vs扩展OzCode输出堆栈日志
  • 使用WinDbg工具分析程序(比较重量级)

3. 阻塞情况

阻塞情况与耗时线程类似,不过代码走查对项目的熟悉程度较高,最简单的方式就是通过vs中断程序找到阻塞点后,按F11激活调试

4. 死锁情况

死锁问题排查也非常简单,排查方法如下:
方法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)

你可能感兴趣的:(.NET,WPF,wpf)