问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

使用Visual Studio调试多线程应用程序

创作时间:
作者:
@小白创作中心

使用Visual Studio调试多线程应用程序

引用
1
来源
1.
https://learn.microsoft.com/zh-cn/visualstudio/debugger/get-started-debugging-multithreaded-apps?view=vs-2019

本文将详细介绍如何使用Visual Studio调试多线程应用程序。通过本文,你将学习到如何使用线程标记、并行堆栈窗口、并行监视窗口、条件断点等工具,以及如何创建和调试多线程应用项目。本文提供了C#、Visual Basic和C++三种语言的代码示例,适合有一定编程基础的技术人员阅读。

创建一个多线程应用项目

  1. 打开Visual Studio并创建一个新项目。如果“开始”窗口未打开,请选择“文件”>“启动窗口”。
  2. 在“开始”窗口上,选择“创建新项目”。
  3. 在“创建新项目”窗口的搜索框中输入或键入“控制台”。接下来,从“语言”列表中选择“C#”、“C++”或“Visual Basic”,然后从“平台”列表中选择“Windows”。
  4. 应用语言和平台筛选器之后,对.NET或C++选择“控制台应用”模板,然后选择“下一步”。
  5. 在“配置新项目”窗口中,在“项目名称”框中键入或输入MyThreadWalkthroughApp。然后,选择“下一步”或“创建”(视具体提供的选项而定)。
  6. 对于.NET Core或.NET 5+项目,选择建议的目标框架或.NET 8,然后选择“创建”。

新的控制台项目随即显示。创建该项目后,将显示源文件。根据所选语言,源文件名称可能是Program.cs、MyThreadWalkthroughApp.cpp或Module1.vb。

  1. 删除源文件中显示的代码,并将其替换为以下更新的代码。为代码配置选择适当的代码片段。

C#代码示例

using System;
using System.Threading;

public class ServerClass
{
    static int count = 0;
    // The method that will be called when the thread is started.
    public void InstanceMethod()
    {
        Console.WriteLine(
            "ServerClass.InstanceMethod is running on another thread.");
        int data = count++;
        // Pause for a moment to provide a delay to make
        // threads more apparent.
        Thread.Sleep(3000);
        Console.WriteLine(
            "The instance method called by the worker thread has ended. " + data);
    }
}

public class Simple
{
    public static void Main()
    {
        for (int i = 0; i < 10; i++)
        {
            CreateThreads();
        }
    }

    public static void CreateThreads()
    {
        ServerClass serverObject = new ServerClass();
        Thread InstanceCaller = new Thread(new ThreadStart(serverObject.InstanceMethod));
        // Start the thread.
        InstanceCaller.Start();
        Console.WriteLine("The Main() thread calls this after "
            + "starting the new InstanceCaller thread.");
    }
}

Visual Basic代码示例

Imports System.Threading

Public Class ServerClass
    ' The method that will be called when the thread is started.
    Public count = 0
    Public Sub InstanceMethod()
        Console.WriteLine(
                "ServerClass.InstanceMethod is running on another thread.")
        Dim data = count + 1
        ' Pause for a moment to provide a delay to make
        ' threads more apparent.
        Thread.Sleep(3000)
        Console.WriteLine(
                "The instance method called by the worker thread has ended. " + data)
    End Sub
End Class

Public Class Simple
    Public Shared Sub Main()
        Dim ts As New ThreadStarter
        For index = 1 To 10
            ts.CreateThreads()
        Next
    End Sub
End Class

Public Class ThreadStarter
    Public Sub CreateThreads()
        Dim serverObject As New ServerClass()
        ' Create the thread object, passing in the
        ' serverObject.InstanceMethod method using a
        ' ThreadStart delegate.
        Dim InstanceCaller As New Thread(AddressOf serverObject.InstanceMethod)
        ' Start the thread.
        InstanceCaller.Start()
        Console.WriteLine("The Main() thread calls this after " _
                    + "starting the new InstanceCaller thread.")
    End Sub
End Class

C++代码示例

#include <thread>
#include <iostream>
#include <vector>
#include <string>

int count = 0;

void doSomeWork() {
    std::cout << "The doSomeWork function is running on another thread." << std::endl;
    int data = count++;
    // Pause for a moment to provide a delay to make
    // threads more apparent.
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::string str = std::to_string(data);
    std::cout << "The function called by the worker thread has ended. " + str << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.push_back(std::thread(doSomeWork));
        std::cout << "The Main() thread calls this after starting the new thread" << std::endl;
    }
    for (auto& thread : threads) {
        thread.join();
    }
    return 0;
}
  1. 在“文件”菜单上,单击“全部保存”。

  2. (仅适用于Visual Basic)在“解决方案资源管理器”(右窗格)中,右键单击项目节点,然后选择“属性”。在“应用程序”选项卡下,将“启动对象”更改为“简单”。

调试多线程应用

  1. 在源代码编辑器中,查找以下代码片段:
Thread.Sleep(3000);
Console.WriteLine();
Thread.Sleep(3000)
Console.WriteLine()
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "The function called by the worker thread has ended." << std::endl;
  1. 在Thread.Sleep语句或std::this_thread::sleep_for语句(针对C++)的左滚动条槽中单击左键,以插入新的断点。在滚动条槽中,红色圆圈表示已在该位置设置了一个断点。

  2. 在“调试”菜单上,单击“开始调试(F5)”。Visual Studio将生成该解决方案,应用在附加了调试器的情况下开始运行,然后在断点处停止。

  3. 在源代码编辑器中,找到包含断点的行。

发现线程标记

  1. 在调试工具栏中,单击“在源中显示线程”按钮。

  2. 按F11两次以完成调试器的下一步。

  3. 查看窗口左侧的滚动条槽。在此行中,注意“线程标记”图标,类似于一条双绞线。线程标记指示线程在此位置停止。线程标记可以被断点部分隐藏。

  4. 将指针悬停在线程标记上。此时会出现一个数据提示,告知你每个已停止线程的名称和线程ID号。在这种情况下,名称可能是

  5. 选择线程标记,以查看快捷菜单上的可用选项。

查看线程位置

在“并行堆栈”窗口中,可以在“线程”视图和“任务”视图(适用于基于任务的编程)之间进行切换,并且可以查看每个线程的调用堆栈信息。在此应用中,我们可以使用“线程”视图。

  1. 通过选择“调试”>“窗口”>“并行堆栈”,打开“并行堆栈”窗口。此时会看到如下所示的内容。确切信息可能因每个线程的当前位置、硬件和编程语言而异。

在此示例中,从左到右会看到托管代码的以下信息:

  • 当前线程(黄色箭头)已进入ServerClass.InstanceMethod。可以通过将鼠标悬停在ServerClass.InstanceMethod上来查看线程的线程ID和堆栈帧。
  • 线程31724正在等待线程20272拥有的锁。
  • 主线程(左侧)已在[外部代码]上停止,如果选择“显示外部代码”,则可以详细查看这些代码。

在此示例中,从左到右会看到托管代码的以下信息:

  • 主线程(左侧)已在Thread.Start处停止,停止点由线程标记图标标识。
  • 两个线程已进入ServerClass.InstanceMethod,其中一个线程是当前线程(黄色箭头),另一个线程已停止在Thread.Sleep中。
  • 新线程(右侧)也已启动,但是停止在ThreadHelper.ThreadStart上。
  1. 若要在线列表视图中查看线程,请选择“调试”>“Windows”>“线程”。

在此视图中,可以轻松看到线程20272是主线程,当前位于外部代码中,特别是System.Console.dll。

注意:有关使用“线程”窗口的详细信息,请参阅演练:调试多线程应用程序。

右键单击“并行堆栈”或“线程”窗口中的条目,查看快捷菜单上的可用选项。

可以从这些右键单击菜单中执行各种操作。对于本教程,在“并行监视”窗口(后续各节)中详细探索这些详细信息。

对变量设置监视

  1. 通过选择“调试” >“窗口” >“并行监视” >“并行监视 1”,打开“并行监视”窗口。

  2. 选择文本所在的单元格(或第4列中的空标头单元格),输入data。

窗口中会显示每个线程的数据变量的值。

  1. 选择文本所在的单元格(或第5列中的空标头单元格),输入count。

窗口中会显示每个线程的count变量的值。如果看不到这么多的信息,请尝试按F11几次以继续在调试器中执行线程。

  1. 右键单击窗口中的某一行以查看可用选项。

标记线程和取消标记线程

可以通过标记线程来追踪重要的线程,并忽略其它线程。

  1. 在“并行监视”窗口中,按住Shift键并选择多行。

  2. 右键单击并选择“标记”。

系统会标记所有选定的线程。现在,可以进行筛选以仅显示标记的线程。

  1. 在“并行监视”窗口中,选择“仅显示标记的线程”按钮。

列表中仅显示标记的线程。

提示:在标记一些线程后,可以右键单击代码编辑器中的代码行,然后选择“将标记的线程运行到光标处”。请确保选择所有已标记的线程将达到的代码。Visual Studio将在选择的代码行处暂停线程,这样就可以通过冻结和解冻线程更容易地控制执行顺序。

  1. 再次选择“仅显示标记的线程”按钮,以切换回“显示全部线程”模式。

  2. 若要取消标记线程,请在“并行监视”窗口右键单击一个或多个已标记线程,然后选择“取消标记”。

冻结和解冻线程执行

提示:可以通过冻结和解冻(暂停和恢复)线程来控制线程执行工作的顺序。这有助于解决并发问题,例如死锁和争用条件。

  1. 在“并行监视”窗口中,在选中所有行的情况下,右键单击并选择“冻结”。

在第二个列中,每个行出现一个暂停图标。暂停图标指示该线程已冻结。

  1. 仅选择一行,取消选中其他行。

  2. 右键单击某行并选择“解冻”。

暂停图标在此行上消失,表明线程已不再被冻结。

  1. 切换到代码编辑器,按F11。仅运行未冻结的线程。

应用可能还实例化某些新线程。任何新线程均处于未标记状态,不会被冻结。

使用条件断点跟踪单个线程

可以在调试器中对单线程的执行情况进行跟踪。一种方法是冻结不感兴趣的线程。在某些情况下,可能需要在不冻结其他线程的情况下跟踪单个线程,例如重现特定Bug。要在一个不冻结其他线程的情况下跟踪某个线程,必须避免在不感兴趣的线程上中断。可以通过设置条件断点来执行此任务。

可以根据不同的条件(例如,线程名称或线程ID)来设置断点。对于已知对每个线程唯一的数据,设置该条件会有所帮助。当你对某些特定数据值比对任何特定线程更感兴趣时,这种方法在调试过程中很常见。

  1. 右键单击先前创建的断点,然后选择“条件”。

  2. 在“断点设置”窗口中,输入data == 5作为条件表达式。

提示:如果对特定的线程更感兴趣,则请使用线程名称或线程ID作为条件。要在“断点设置”窗口中执行此操作,请选择“筛选器”而不是“条件表达式”,并按照筛选器提示操作。可能需要在应用代码中指定线程名称,因为线程ID在重启调试程序时会更改。

  1. 关闭“断点设置”窗口。

  2. 选择重启按钮以重启调试会话。

在数据变量的值为5的线程中,中断代码执行。请在“并行监视”窗口中,寻找表示当前调试器上下文的黄色箭头。

  1. 现在,可以逐过程执行代码(F10)和单步执行代码(F11),并跟踪单个线程的执行情况。

只要断点条件对于线程是唯一的,并且调试器不会在其他线程上命中其他任何断点(你可能需要禁用它们),你就可以逐过程执行代码和单步执行代码,而无需切换到其他线程。

注意:调试器前进时,所有线程都将运行。但是,调试器不会中断其他线程上的代码,除非其中一个线程命中断点。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号