Visual Studio 高级调试-代码调试

概述

编程圈子里隔三差五的就会有场“谁是最强IDE”之争,重要的是我们需要对使用的IDE有充分的了解,正所谓工欲善其事,必先利其器。

本文主要讲述Visual Studio常用的调试技巧,包括多类型断点,数据监视,以及多线程调试等。下篇则侧重于性能分析和Visual Studio Enterprise 版本所特有的一些高级功能。

调试原理

PDB文件

PDB被称为符号文件或程序数据库文件,它主要用来保存源代码和二进制指令文件(dll)之间的映射关系。主要用途有两个:1.指出二进制指令所对应的代码位置,即源文件和行号;2.帮调试器确定断点位置。

Visual Studio在Release模式下也会生成.pdb文件,但这并不意味着我们项目发布需要它。事实上,由于.pdb是编译时与.dll一起生成的,因此它只会记录当时代码与dll的映射关系,随着存储库代码的变更,它的价值会越来越低。并且它是保存有编译机器上的代码路径信息的,因此存在安全隐患。

有同学会将堆栈信息与pdb的关系搞混,异常发生时的堆栈信息是CLR生成的,跟pdb没有任何关系。

Visual Studio要求pdb版本与所调试代码版本必须完全一致。

调试外部代码

Visual Studio默认开启仅我的代码调试,如果希望能直接调试第三方代码,需要开启外部代码调试:

  1. 调试 ->常规 -> 启用.NET Framework单步执行 & 启用源服务器支持

  1. 调试 -> 符号 -> 符号文件(.pdb)位置,勾选Microsoft 和Nuget符号服务器

启动调试后,Visual Studio会首先将第三方模块的pdb文件下载到本地目录,再根据pdb文件中指出的源文件地址,到源文件服务器下载源文件,这一切完成之后,我们的断点才可以被命中,之后再按F11即可调试第三方源代码。

启用外部代码调试通常比较耗时,一般不推荐使用。

调试System.Collections.Generic.List<T>源码

断点

一切调试的前提都基于断点,Visual Studio为我们提供了多种类型的断点,熟悉不同类型的断点并在合适的场景下运用它们,可以提高调试效率。

★表达式断点★

用当前可访问对象来设置一条bool表达式,当表达式条件为true时才会命中的断点

命中次数断点

为循环语句而设计的断点,假如你认为程序循环执行到一定次数将会出错时可以使用这种断点。

一次性断点

顾名思义,命中一次之后会自动消失的断点。

前置依赖断点

必须先命中指定的断点,才能被命中的断点。

函数名断点

当函数有多个重载或函数名称重复时,可以设置函数名断点,它会在调试时自动为所有函数设置断点。

函数名断点

数据监视

调试的目的是为了观察程序当前的运行状态,在Visual Studio中我们主要通过以下几种窗口来观察当前数据。

局部变量窗口

我们可以查看当前作用域内的对象或者变量值。

监视窗口

当前数据较多时,更方便的查看某个对象或者变量值;也可用来追踪超范围对象,监视函数返回值等。

★即时窗口★

可以对当前作用域内的元素进行运算,或者执行某个方法获取结果,为我们推断程序运行结果,排查复杂问题提供了极大的便利。

自动窗口

可以在函数执行完之后查看到函数的返回值,而不必在调试时创建一个变量来查看执行结果。

超范围对象跟踪

在局部变量窗口为对象创建一个id,并且添加到监视变量窗口当中。这样即使变量离开了当前代码的监测范围,我们依然可以观察到该对象的实时状态。

集合可视化

从Visual Studio 2022开始支持集合对象的可视化,集合中的元素可以用表格的形式展现在VS中,并且支持数据导出。

多线程调试

多线程调试则是开发者必须要掌握的重要技能。Visual Studio用于调试线程的主要工具有线程窗口、线程标记、并行堆栈窗口、并行监视窗口等,我们可以查看并行堆栈信息,进行线程标记,切换,冻结等操作。

模拟多线程场景

同学们应该都了解过线程安全集合与非线程安全集合相关知识,我们先用以下代码来模拟一个非线程安全的List。

class SampleList
{
    public int Index = 0;
    public int[] Array = new int[100];

    public void Add(int i)
    {
        if(Index > 99)
        {
            return;
        }

        Array[Index] = i;
        Index++;

        Console.WriteLine($"Insert {i} at array index:{Index}");
    }
}

然后我们在Main函数中创建1个SampleList,使用5个线程同时插入数据,观察不同线程中Index和Array的值。

var sample = new SampleList();
for(int i = 0; i < 5; i++)
{
    new Thread(() =>
    {
        for(int j = 0; j < 20; j++)
        {
            sample.Add(1);
            Thread.Sleep(500);
        }
    }).Start();
}

Console.ReadKey();

在源中显示线程

点击Visual Studio菜单中的"双绞线"图标,开启在源中显示线程。

它的作用是在源代码中标记不同的线程正在执行的位置,和断点显示的位置一样,它会显示一个双绞线图标。我们还可以右击该图标进行线程切换。

线程窗口

开始调试,等程序运行几秒钟后打上断点,在线程窗口我们可以观察到进程当前的所有线程状态。我们可以查看线程ID,类型,当前所在位置等,你还可以选择窗口上方的"列"来展示更多信息。

当我们命中断点之后,所有的线程都会停下来。展开线程调用堆栈,我们可以清楚的观察到当前有五个线程正在执行SampleList.Add() 方法,除了主线程外,还有一些.NET项目自身的基础线程在工作。

并行堆栈

并行堆栈展示的是当前线程之间的调用关系,以及堆栈信息。我们可以切换到视图模式,可以导出。并且因为C#中具有更高级别的抽象"线程":Task, 并行堆栈窗口还支持任务这种异步逻辑堆栈。

并行监视

前面我们已经观察到,当前有5个线程正在执行SampleList.Add()方法,而监视窗口只能观察到当前线程的数据,因此我们需要用到并行监视窗口。窗口中会显示每个线程中 Index和Array 的值。

线程标记

假如我们希望只看执行SampleList.Add() 方法方法的5个线程,则可以将它们设置为标记状态,该状态可以用于筛选线程。选择"仅显示标记线程"之后,并行监视窗口也会同步筛选条件。

跟踪单个线程

在别处再打一个断点,此时你按F5进行调试时,会发现调试器将以乱序进行工作,有没有办法在同一线程内进行调试呢?我们可以利用前面所提到的条件断点来实现这一操作。

使用筛选器,设置ThreadId,这样断点就只会在指定线程执行时命中了。

冻结线程

有时候,我们希望某个线程停下来,或者仅让一个线程单独执行,或者让线程以我们想要的顺序来执行,以此来观察程序状态,比如解决死锁问题等。

这时候可以利用线程冻结操作来完成。如图所示,我冻结了其余4个线程,仅让线程23908执行,此时使用F10、F11调试就与我们熟悉的单线程调试的情况无异了。

当前线程23908正在执行第26行,而其他线程则冻结在了34行。

冻结线程23908,解冻线程4744, 按F11, 调试将从线程4744堆栈所指示的位置(第34行)往下执行。

多任务调试

因为有更高级别的抽象库:System.Threading.Tasks.Task,所以C#中很少直接使用Thread来编写并行代码。Task并不是与线程一一对应的,一个线程可能会执行多个Task(C#并行编程: 从线程,线程池到任务_郭麻花的博客),Visual Studio同样支持基于Task类型的多线程调试。

并行堆栈窗口

将代码改造成Task模式,我们可以在并行堆栈窗口查看执行Task的线程信息:

任务窗口

监视窗口

方法视图

我们还可以通过切换并行堆栈窗口为方法视图,查看当前任务调用关系。可以看到由主线程产生五个异步任务正在执行SampleList.Add(), 而其中一个正在执行Console.WriteLine(), 我们可以清楚的看到Console.WriteLine()的调用堆栈信息。

热重载

热重载指的是在程序运行时可以直接修改源代码并且继续运行,无需重新启动调试。目前绝大部分.NET项目都支持热重载技术。

异常中断

即使我们在程序中使用了try-catch,但在调试时可能仍希望出现异常时能够及时的中断,以便排查问题。我们只需要在Visual Studio使用 调试 -> 异常设置 将指定异常设置成引发中断即可,并且异常可以用dll名称作为触发筛选条件。

总结

到这里我们介绍了Visual Studio中常用的代码调试技巧,Visual Studio还有支持远程调试,附加进程调试,多进程调试等,因为我不常用,所以就不介绍了。

实际开发当中调试技术并不是最重要的,最重要的是调试思路,思路(70%)+技巧(30%)=更高效的调试。就好比中医所讲的“望闻问切”,观察现象,思考原因,查看代码,调试是最后一步。

猜你喜欢

转载自blog.csdn.net/qq_40404477/article/details/129051971