Debug调试之道:使用Windows API排查程序的线程泄露

本文介绍了Windows系统中程序线程泄露的排查方法。首先分析了线程泄露的三大原因(未正确终止、线程池管理不当、资源依赖未解除)及其后果(资源耗尽、性能下降、系统不稳定)。随后讲解了两种排查工具:Windows资源监视器和Procexp进程资源管理器。重点给出了通过Windows API编写的代码解决方案,包括线程信息结构体定义、线程快照捕获、线程状态比对等核心功能,可输出线程ID、内存地址及所属模块信息,帮助准确定位泄露源。最后演示了如何将监控信息输出到界面或日志中,便于实时分析线程变化情况。

作者
WildPointer
发布
2025.08.24
专栏
Debug调试之道
阅读
约 13 分钟 / 1,246 次原文浏览
Debug调试之道:使用Windows API排查程序的线程泄露

前言

本篇博客主要是讲解如何排查在Windows中程序线程泄露导致异常的情况,由于很多第三方库会存在自己的线程库,并且采用多线程的方式去优化自己算法的执行效率。这个时候我们很难分辨是哪些模块发生了线程创建不释放的情况,只能通过调用Windows API来输出相关的模块信息,所以本篇博客就是将排查Windows中程序线程泄露的思路和代码进行分享


线程泄露的原因和后果

导致线程泄露的主要原因有以下几个:
1.线程未正确终止:线程执行完毕后未调用 CloseHandle(Windows API)或未触发线程退出条件(如未设置终止标志位)

2.线程池管理不当:未使用线程池时,频繁手动创建线程且未回收

3.资源依赖未解除:线程持有对象强引用,导致线程对象无法被垃圾回收

最终会导致进程发生以下异常:
1.资源耗尽:每个线程占用约1MB栈内存(可配置),大量泄露导致进程内存耗尽,并且线程内核对象句柄超限(默认单进程约10,000个),后续线程创建将会失败

2.性能劣降低:泄露线程可能空转或频繁上下文切换,占用CPU资源但无实际工作,或者线程调度开销增大,主线程因资源竞争被阻塞,导致界面卡顿或无响应

3.系统稳定性降低:内存或句柄耗尽触发系统强制终止进程


通过Windows自带的资源监视器监控线程泄露

当我们怀疑进程发生线程泄露时,我们可以通过Windows自带的资源监视器来分析是否是线程泄露,可以在资源监视器中勾选要监控的进程(以百度翻译为例),在线程数一列中可以看到该进程当前的线程总数 

PS:通过监视器只能判断是否是线程泄露,并不能知道是哪一个模块发生了线程泄露       

图1.资源监视器判断线程泄露


通过Procexp进程资源管理器分析线程泄露

除去Windows自带的资源监视器以外,我们还可以通过进程资源管理器Procexp进行分析。此处以微信为例,右键要排查的进程,并点击Properties后在Threas选项中查看当前进程的线程数和所属模块。

图2.使用Procexp排查线程泄露

可能有些读者还没有下载过该工具,以下是Procexp的下载链接:

进程资源管理器 - Sysinternals | Microsoft Learnhttps://learn.microsoft.com/zh-cn/sysinternals/downloads/process-explorer


使用Windows API编写代码排查线程泄露

以上两种方式只是通过工具的形式去排查线程泄露的问题,而很多时候发生线程泄露我们并不能很好的分析发生的原因,例如在客户的离线环境上。此时则需要我们的进程本身支持对线程监控的功能,把线程的信息输出到日志中,具体头文件如下:

cpp
/** * @struct ThreadInfo * @brief 存储单个线程的核心信息 * @details 用于记录线程ID、所属模块及内存起始地址,便于线程行为分析*/struct ThreadInfo {    quint64 tid;           // 线程ID    QString moduleName;    // 线程所属模块(动态库)的文件名    quint64 startAddress;  // 线程入口函数的起始内存地址};/** * @struct ThreadDiff * @brief 记录线程状态变化信息 * @details 通过对比两次线程快照,标识新增和终止的线程*/struct ThreadDiff {    QVector<ThreadInfo> newThreads;   // 新增线程列表    QVector<ThreadInfo> deadThreads;  // 已终止线程列表};/*----------    测试信息-------------*/class testInfo : public QObject {    Q_OBJECTpublic:    explicit testInfo(QObject* parent = nullptr);    // 检测新增/消失线程    ThreadDiff diffThreads();    // 线程检测    QVector<ThreadInfo> listThreads();    // 新增线程检测    QVector<ThreadInfo> listNewThreads();private:#ifdef Q_OS_WIN  //仅在 Windows 平台下被编译和生效    // 根据内存地址解析所属模块    QString getModuleFromAddress(quint64 addr);#endif    // 保存上一次的线程ID集合    QSet<quint64> lastThreadIds;    // 上次完整线程信息    QVector<ThreadInfo> lastThreadList;};

由于仅在Windows中生效,所以需要定义宏来保证系统一致。具体的源文件如下:

cpp
#ifdef Q_OS_WIN#include <windows.h>#include <tlhelp32.h>#include <psapi.h>#include <QDebug>testInfo::testInfo(QObject* parent) : QObject(parent) {}// NtQueryInformationThread 使用的参数枚举typedef enum _THREADINFOCLASS {    ThreadBasicInformation = 0,          // 基础线程信息    ThreadQuerySetWin32StartAddress = 9  // 获取线程起始地址} THREADINFOCLASS;/*! * @typedef NtQueryInformationThreadPtr * @brief NtQueryInformationThread函数原型定义 * @param ThreadHandle 线程句柄 * @param ThreadInformationClass 信息类型 * @param ThreadInformation 输出信息缓冲区 * @param ThreadInformationLength 缓冲区长度 * @param ReturnLength 实际返回数据长度 * @return 状态码(0表示成功) */typedef LONG(WINAPI* NtQueryInformationThreadPtr)(    HANDLE ThreadHandle,    THREADINFOCLASS ThreadInformationClass,    PVOID ThreadInformation,    ULONG ThreadInformationLength,    PULONG ReturnLength);/*! * @brief 获取当前进程所有线程信息 * @return QVector<ThreadInfo> 线程信息列表,包含: *         - 线程ID *         - 起始地址 *         - 所属模块名 */QVector<ThreadInfo> testInfo::listThreads() {    QVector<ThreadInfo> threads;    DWORD pid = GetCurrentProcessId();  // 获取当前进程ID    // 创建系统线程快照    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);    if (snapshot == INVALID_HANDLE_VALUE) return threads;    THREADENTRY32 te;    te.dwSize = sizeof(te);    // 遍历线程快照    if (Thread32First(snapshot, &te)) {        do {            // 筛选属于当前进程的线程            if (te.th32OwnerProcessID == pid) {                HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, te.th32ThreadID);                if (hThread) {                    // 动态加载NTDLL函数                    HMODULE hNtDll = GetModuleHandleA("ntdll.dll");                    NtQueryInformationThreadPtr NtQueryInformationThread =                        (NtQueryInformationThreadPtr)GetProcAddress(hNtDll, "NtQueryInformationThread");                    if (NtQueryInformationThread) {                        PVOID startAddr = nullptr;                        // 查询线程起始地址                        if (NtQueryInformationThread(hThread,                            ThreadQuerySetWin32StartAddress,                            &startAddr,                            sizeof(startAddr),                            nullptr) == 0) {                            ThreadInfo info;                            info.tid = te.th32ThreadID;                            info.startAddress = (quint64)startAddr;                            info.moduleName = getModuleFromAddress((quint64)startAddr);  // 解析所属模块                            threads.push_back(info);                        }                    }                    CloseHandle(hThread);  // 关闭线程句柄                }            }        } while (Thread32Next(snapshot, &te));  // 遍历下一个线程    }    CloseHandle(snapshot);  // 关闭快照句柄    return threads;}/*! * @brief 检测新增线程(与上次调用比较) * @return QVector<ThreadInfo> 新增线程列表 * @note 内部维护上次线程ID集合用于比对 */QVector<ThreadInfo> testInfo::listNewThreads() {    QVector<ThreadInfo> allThreads = listThreads();    QSet<quint64> currentIds;    QVector<ThreadInfo> newThreads;    for (const auto& t : allThreads) {        currentIds.insert(t.tid);        // 检测不在上次记录中的线程        if (!lastThreadIds.contains(t.tid)) {            newThreads.push_back(t);  // 记录新线程        }    }    lastThreadIds = currentIds;  // 更新线程ID记录    return newThreads;}/*! * @brief 检测线程状态变化(新增和终止) * @return ThreadDiff 结构体包含: *         - newThreads: 新增线程列表 *         - deadThreads: 终止线程列表 */ThreadDiff testInfo::diffThreads() {    ThreadDiff diff;    QVector<ThreadInfo> currentList = listThreads();    QSet<quint64> currentIds;    // 检测新增线程    for (const auto& t : currentList) {        currentIds.insert(t.tid);        if (!lastThreadIds.contains(t.tid)) {            diff.newThreads.push_back(t);        }    }    // 检测终止线程    for (const auto& t : lastThreadList) {        if (!currentIds.contains(t.tid)) {            diff.deadThreads.push_back(t);        }    }    // 更新线程记录    lastThreadIds = currentIds;    lastThreadList = currentList;    return diff;}/*! * @brief 根据内存地址解析所属模块 * @param addr 待查询的内存地址 * @return QString 模块文件名(不含路径) */QString testInfo::getModuleFromAddress(quint64 addr) {    HMODULE hMods[1024];    DWORD cbNeeded;    // 枚举当前进程所有模块    if (EnumProcessModules(GetCurrentProcess(), hMods, sizeof(hMods), &cbNeeded)) {        for (unsigned int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {            MODULEINFO mi;            // 获取模块信息            if (GetModuleInformation(GetCurrentProcess(), hMods[i], &mi, sizeof(mi))) {                quint64 start = (quint64)mi.lpBaseOfDll;                quint64 end = start + mi.SizeOfImage;                // 检查地址是否在模块范围内                if (addr >= start && addr <= end) {                    char modName[MAX_PATH];                    // 获取模块完整路径                    GetModuleFileNameA(hMods[i], modName, sizeof(modName));                    QString fullPath = QString::fromLocal8Bit(modName);                    return QFileInfo(fullPath).fileName(); // 提取纯文件名                }            }        }    }}#endif

对此,我们还需要在main文件中使用编写的接口函数。代码如下:

cpp
testInfo monitor;auto threads = monitor.listNewThreads();QString info = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz") + "\n";info += QString("当前线程总数:[%1]").arg(monitor.listThreads().size()) + "\n";info += QString("全局线程池活跃线程数[%1]").arg(QThreadPool::globalInstance()->activeThreadCount()) + "\n";ThreadDiff diff = monitor.diffThreads();if (!diff.newThreads.isEmpty()) {    info += "新增线程:\n";    for (auto& t : diff.newThreads) {        info += QString("    TID:[%1]    Addr:[0x%2]    Module:[%3]\n").arg(t.tid).arg(QString::number(t.startAddress, 16)).arg(t.moduleName);    }}if (!diff.deadThreads.isEmpty()) {    info += "消失线程:\n";    for (auto& t : diff.deadThreads) {        info += QString("    TID:[%1]    Addr:[0x%2]    Module:[%3]\n").arg(t.tid).arg(QString::number(t.startAddress, 16)).arg(t.moduleName);    }}ui->textEdit->append(info);    // 输出到界面中

PS:可以选择输出到界面控件中或者日志中,并且也可以通过定时器来限制打印线程信息的频率,减少性能的占用。通过输出的信息来排查究竟是哪一个模块导致的线程泄漏,并进行针对性的解决

32次原文点赞;这里的喜欢仅保存在本机
WildPointer

专注系统编程、工程实践与底层技术,记录 C++、Qt、OpenCV 与 VTK 的学习和实践。