☰
C++ WinHTTP多线程网页抓取模块:工业级嵌入式数据采集骨架
2026/10/20 4:42:22 网站建设 项目流程

简介:本资源是一份基于Visual C++环境的C++多线程网页抓取实践代码包,面向具备C++基础、希望掌握网络数据采集与并发编程的中初级开发者。它完整实现了利用std::thread创建多线程、libcurl发起HTTP请求下载HTML页面、pugixml解析响应内容的核心链路,适用于网络爬虫原型开发、批量网页采集等实际场景。压缩包共16个文件(22KB),含5个头文件(h)封装线程控制与下载逻辑、3个源码文件(cpp)实现主流程与回调处理、1个DSP工程配置及配套RC资源、BAT批处理脚本和HTML说明文档等,结构紧凑,便于快速编译运行与源码研读。目前已有171人学习下载,读者可直接复用线程管理框架、HTTP会话封装模板及HTML内容提取示例,同时获得VC6.0兼容工程结构与典型错误处理思路,是理解C++网络编程落地细节的优质入门参考。

1. 这不是爬虫脚本,而是一个可嵌入工业级数据采集模块的 C++ 多线程网页抓取骨架

某公司做设备状态监控系统时,需要每 3 秒轮询 200+ 个嵌入式 Web 管理页(HTTP 接口返回纯 HTML 表单或 JSON 响应),但 Python requests + asyncio 在 Windows 服务环境下频繁出现连接复用失败、SSL handshake 超时、内存泄漏问题,最终回退到 C++ 实现——这个「c++多线程抓取网页代码」项目,就是他们剥离出的最小可运行核心:不依赖 Qt 或 Boost,仅用 Windows API + WinHTTP(VC 编译器原生支持)构建线程安全的 HTTP 客户端池,支持自定义 User-Agent、Cookie 持有、超时控制、重试策略和响应体编码自动识别(GBK/UTF-8/ISO-8859-1)。它不是教你怎么写爬虫,而是告诉你:当你要把网页抓取能力塞进一个长期运行的 C++ 后台服务、DLL 插件或硬件网关固件里时,该怎么从零搭起第一块砖。适合正在用 VC 开发 Windows 桌面工具、工控上位机、网络设备配置代理模块的开发者,尤其当你被std::thread和WinHttpSendRequest的句柄生命周期搞到凌晨三点改崩溃日志时——这份代码能直接救你一命。


2. 架构与编译环境:为什么选 WinHTTP 而不是 libcurl 或 C++ REST SDK

这个项目没用任何第三方库,所有网络层调用直连 Windows 系统 API。这不是为了炫技,而是源于真实产线约束:目标设备是 x86 架构的嵌入式 Windows CE 兼容系统(实际为某定制 Win10 IoT 版本),不允许部署动态链接库(DLL),且必须静态链接、无运行时依赖。libcurl 虽好,但默认依赖 OpenSSL 动态库;C++ REST SDK(cpprestsdk)体积大、依赖 Visual C++ Redistributable 版本敏感,在老旧工控机上极易触发MSVCP140.dll not found。而 WinHTTP 是 Windows 自带组件,从 XP SP2 到 Win11 全系预装,头文件<winhttp.h>随 Visual Studio 安装即得,链接时只需加/link winhttp.lib——这才是真正“开箱即用”的底层能力。

2.1 模块分层设计:三层解耦,便于替换与测试

整个工程按职责划分为三个物理目录(非命名空间):

/src /core ← 核心网络引擎(WinHTTP 封装、会话管理、请求队列) /parser ← HTML 解析辅助(轻量 DOM 查找、正则提取、编码探测) /example ← 可执行示例(main.cpp + 配置驱动)

提示:/parser目录下没有用 HtmlAgilityPack 或 Gumbo Parser,只提供HtmlExtractor::ExtractByTag()和HtmlExtractor::DetectEncoding()两个函数。前者用std::string::find()手动定位标签边界(适合结构固定的监控页),后者通过<meta charset="...">和 BOM 头双重判断——够用、无依赖、易调试。

2.2 编译配置:VC 2015 及以上均可,但必须关闭 SDL 检查

项目使用 C++14 语法(auto、constexpr、std::shared_ptr),在 Visual Studio 中需确认以下设置:

  • 平台工具集:v140(VS2015)或更高(v142/v143)
  • C++ 语言标准:ISO C++14 标准(/std:c++14)
  • 预处理器定义:WIN32;_WINDOWS;UNICODE;_UNICODE
  • 附加依赖项:winhttp.lib
  • 关键禁用项:在「配置属性 → 常规 → SDL 检查」中设为“否”。否则WinHttpOpen()返回的HINTERNET句柄会被误判为“未初始化指针”,导致编译期报错 C4703(使用了可能未初始化的本地指针变量)。

下面这段是core/HttpClient.h中最核心的类声明节选,它决定了整个项目的线程安全边界:

class HttpClient { public: explicit HttpClient(DWORD dwFlags = WINHTTP_FLAG_ASYNC); ~HttpClient(); // 同步 GET 请求(阻塞调用,适合单次任务) bool Get(const std::wstring& url, std::string& out_body, int timeout_ms = 5000); // 异步请求提交(线程池内执行,回调通知) bool AsyncGet(const std::wstring& url, std::function<void(bool, const std::string&)> callback, int timeout_ms = 5000); // 线程安全的 Cookie 设置(内部用 CRITICAL_SECTION 保护) void SetCookie(const std::wstring& domain, const std::string& cookie_str); private: HINTERNET m_hSession; // WinHTTP 会话句柄(全局唯一) HINTERNET m_hConnect; // 连接句柄(按 host 复用) CRITICAL_SECTION m_csCookie;// Cookie 写入锁 std::map<std::wstring, std::string> m_cookies; };

逻辑说明:

  • m_hSession在构造时调用WinHttpOpen()创建,生命周期贯穿对象始终;
  • m_hConnect在首次请求时按域名创建(WinHttpConnect()),后续同域请求复用,避免重复 TCP 握手;
  • SetCookie()使用CRITICAL_SECTION而非std::mutex,是因为该类设计目标是兼容 Windows XP(std::mutex在 XP 上不可用);
  • 所有WinHttp*函数调用后都检查GetLastError(),错误码映射为enum HttpError { ERR_OK, ERR_TIMEOUT, ERR_CONN_REFUSED, ... },方便上层统一处理。

2.3 线程模型:生产者-消费者 + 固定大小线程池

项目不使用std::thread直接创建线程,而是封装了一个ThreadPool类,其核心是 Windows 原生线程池 API(CreateThreadpool,SubmitThreadpoolWork),优势在于:

  • 系统级线程复用,避免频繁CreateThread/CloseHandle开销;
  • 支持SetThreadpoolThreadMaximum()控制最大并发数(默认 8);
  • 工作项(TP_WORK)绑定 C++ lambda 时,需通过std::shared_ptr捕获上下文,防止对象提前析构。

example/main.cpp中的典型用法如下:

int main() { ThreadPool pool(4); // 创建 4 线程工作池 HttpClient client; std::vector<std::future<void>> futures; std::vector<std::wstring> urls = { L"http://192.168.1.10/status.html", L"http://192.168.1.11/status.html", L"http://192.168.1.12/status.html" }; for (const auto& url : urls) { futures.emplace_back(pool.enqueue([url, &client]() { std::string body; if (client.Get(url, body, 3000)) { // 解析 body 中的 <td>Temperature:</td><td>23.5°C</td> auto temp = HtmlExtractor::ExtractByTag(body, "td", 1); // 第二个 td printf("Fetched %S -> temp: %s\n", url.c_str(), temp.c_str()); } })); } // 等待全部完成 for (auto& f : futures) f.wait(); return 0; }

参数说明:

  • ThreadPool(4)表示最多同时运行 4 个抓取任务,超出请求排队;
  • client.Get()是同步接口,但被包裹在pool.enqueue()中,实际由线程池线程执行;
  • HtmlExtractor::ExtractByTag(body, "td", 1)表示查找第 2 个<td>标签内容(索引从 0 开始),适用于固定格式的设备状态页;
  • 所有printf输出需注意:url.c_str()是wchar_t*,必须用%S(大写 S)而非%s,否则乱码。

3. 核心代码逐行解析:从 WinHttpOpen 到响应体解码

这一章我们拆开core/HttpClient.cpp中最关键的Get()函数,看它如何把一个 URL 变成可用的 HTML 字符串。这不是 API 文档搬运,而是站在调试器断点旁,一行行告诉你每个 WinHTTP 调用背后发生了什么、为什么这么写、不这么写会怎样。

3.1 初始化会话与连接:WinHttpOpen 和 WinHttpConnect

bool HttpClient::Get(const std::wstring& url, std::string& out_body, int timeout_ms) { // Step 1: 解析 URL 获取 host/port/path URL_COMPONENTS uc = {0}; uc.dwStructSize = sizeof(uc); uc.lpszHostName = new wchar_t[256]; uc.dwHostNameLength = 256; uc.lpszUrlPath = new wchar_t[1024]; uc.dwUrlPathLength = 1024; if (!WinHttpCrackUrl(url.c_str(), static_cast<DWORD>(url.length()), 0, &uc)) { DWORD err = GetLastError(); delete[] uc.lpszHostName; delete[] uc.lpszUrlPath; return false; // URL 格式错误,如缺少 http:// } // Step 2: 创建会话(全局唯一,复用) if (m_hSession == NULL) { m_hSession = WinHttpOpen(L"DeviceMonitor/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (m_hSession == NULL) { DWORD err = GetLastError(); // 常见:ERROR_WINHTTP_INTERNAL_ERROR(系统资源不足) delete[] uc.lpszHostName; delete[] uc.lpszUrlPath; return false; } } // Step 3: 创建连接(按 host 复用) if (m_hConnect == NULL || wcscmp(m_lastHost.c_str(), uc.lpszHostName) != 0) { if (m_hConnect) WinHttpCloseHandle(m_hConnect); m_hConnect = WinHttpConnect(m_hSession, uc.lpszHostName, uc.nPort == 0 ? INTERNET_DEFAULT_HTTP_PORT : uc.nPort, 0); if (m_hConnect == NULL) { DWORD err = GetLastError(); // 常见:ERROR_WINHTTP_NAME_NOT_RESOLVED(DNS 失败) delete[] uc.lpszHostName; delete[] uc.lpszUrlPath; return false; } m_lastHost = uc.lpszHostName; } delete[] uc.lpszHostName; delete[] uc.lpszUrlPath; // ... 后续请求发送逻辑 }

逻辑说明:

  • WinHttpCrackUrl()是 Windows 提供的 URL 解析函数,比手写正则更可靠,能正确处理http://user:pass@host:port/path?query#frag全格式;
  • m_hSession必须全局复用:若每次请求都WinHttpOpen(),会导致句柄泄漏(Windows 限制每个进程约 16384 个句柄);
  • m_hConnect按 host 复用:同一台设备的多个页面(如/status.html,/config.json)走同一个 TCP 连接,省去三次握手和 TLS 握手开销;
  • m_lastHost是std::wstring成员变量,用于缓存上次连接的 host,避免每次比较都调用wcscmp();
  • 所有new[]分配的内存必须delete[],否则在长时间运行的服务中会缓慢内存泄漏。

3.2 发送请求与读取响应:WinHttpOpenRequest → WinHttpSendRequest → WinHttpReceiveResponse

// Step 4: 创建请求句柄 HINTERNET hRequest = WinHttpOpenRequest(m_hConnect, L"GET", uc.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); if (hRequest == NULL) { return false; // 常见:ERROR_INVALID_PARAMETER(路径含非法字符) } // Step 5: 设置请求头(User-Agent、Cookie) std::wstring headers = L"User-Agent: DeviceMonitor/1.0\r\n"; EnterCriticalSection(&m_csCookie); auto it = m_cookies.find(m_lastHost); if (it != m_cookies.end()) { headers += L"Cookie: " + std::wstring(it->second.begin(), it->second.end()) + L"\r\n"; } LeaveCriticalSection(&m_csCookie); if (!WinHttpAddRequestHeaders(hRequest, headers.c_str(), (ULONG)-1, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { WinHttpCloseHandle(hRequest); return false; } // Step 6: 发送请求 if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { WinHttpCloseHandle(hRequest); return false; // 常见:ERROR_WINHTTP_CANNOT_CONNECT(目标主机拒绝连接) } // Step 7: 等待响应 if (!WinHttpReceiveResponse(hRequest, NULL)) { WinHttpCloseHandle(hRequest); return false; // 常见:ERROR_WINHTTP_TIMEOUT(超时)、ERROR_WINHTTP_CONNECTION_ERROR(网络中断) }

参数说明:

  • WinHttpOpenRequest()的第五个参数WINHTTP_NO_REFERER表示不发送 Referer 头,避免某些设备 Web 服务校验来源;
  • WinHttpAddRequestHeaders()中WINHTTP_ADDREQ_FLAG_REPLACE确保 Cookie 被覆盖而非追加,防止重复设置;
  • WinHttpSendRequest()的倒数第二个参数0表示无请求体(GET 不需要),最后一个参数0表示无额外标志;
  • WinHttpReceiveResponse()是阻塞调用,内部会等待 TCP 数据到达并解析 HTTP 状态行(如HTTP/1.1 200 OK);
  • 所有失败路径都必须调用WinHttpCloseHandle(hRequest),否则句柄泄露——这是新手最常翻车的地方。

3.3 读取响应体并自动解码:WinHttpQueryDataAvailable → WinHttpReadData

// Step 8: 查询响应体长度(非必须,但可预分配 buffer) DWORD dwSize = 0; if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { WinHttpCloseHandle(hRequest); return false; // 常见:ERROR_WINHTTP_INVALID_SERVER_RESPONSE(服务器返回畸形 HTTP) } // Step 9: 循环读取直到 EOF std::vector<char> buffer(8192); std::string response_body; response_body.reserve(dwSize > 0 ? dwSize : 4096); DWORD dwDownloaded = 0; do { dwSize = static_cast<DWORD>(buffer.size()); if (!WinHttpReadData(hRequest, buffer.data(), dwSize, &dwDownloaded)) { break; // 读取完成或出错 } if (dwDownloaded > 0) { response_body.append(buffer.data(), dwDownloaded); } } while (dwDownloaded > 0); // Step 10: 自动检测编码并转为 UTF-8 string std::string utf8_body; if (!HtmlExtractor::ConvertToUtf8(response_body, utf8_body)) { // 备用方案:强制 GBK(常见于国产设备 Web 界面) std::string gbk_body; if (HtmlExtractor::GbkToUtf8(response_body, gbk_body)) { utf8_body = std::move(gbk_body); } else { utf8_body = response_body; // 保持原始字节,由上层决定如何显示 } } out_body = std::move(utf8_body); WinHttpCloseHandle(hRequest); return true;

逻辑说明:

  • WinHttpQueryDataAvailable()返回的是“当前可读字节数”,不是 Content-Length(因为服务器可能 chunked 编码),所以不能直接reserve()精确大小,但能避免频繁std::string::append()导致的多次内存重分配;
  • WinHttpReadData()是循环调用,因为单次最多读buffer.size()字节,而响应体可能达数 MB;
  • HtmlExtractor::ConvertToUtf8()先查<meta charset="gbk">,再查 BOM(\xEF\xBB\xBF是 UTF-8,\xFF\xFE是 UTF-16LE),最后 fallback 到MultiByteToWideChar(CP_ACP, ...);
  • GbkToUtf8()使用MultiByteToWideChar(CP_GB2312, ...)+WideCharToMultiByte(CP_UTF8, ...)两步转换,比直接iconv更轻量;
  • 最终out_body是std::string(UTF-8 编码),可直接传给std::cout、JSON 解析器或 GUI 控件(如 MFCCStringA)。

4. 常见问题排查:我在三台不同工控机上踩过的 5 个真实坑

这个项目在交付前经历了 3 轮现场部署,每一轮都暴露出 Windows 环境下 WinHTTP 的玄学行为。以下是整理出的 5 条血泪经验,按「现象 → 原因 → 解决」结构给出,每一条都对应一个真实崩溃日志或超时截图。

4.1 现象:程序在 Windows 7 SP1 上运行 2 小时后卡死,CPU 占用 100%,调试器显示线程停在WinHttpReceiveResponse()

原因:Windows 7 SP1 的 WinHTTP 存在一个已知 Bug:当服务器返回Connection: close且响应体未完整读取时,WinHttpReceiveResponse()会陷入无限等待。该 Bug 在 KB2533623 补丁中修复,但很多工控机禁止自动更新。
解决:在WinHttpReceiveResponse()后立即调用WinHttpQueryHeaders()获取Content-Length,若存在且大于 0,则用WinHttpReadData()循环读取精确字节数,读完即停;若不存在,则设置WINHTTP_OPTION_RECEIVE_TIMEOUT为 5000ms,避免死等。

4.2 现象:抓取 HTTPS 页面时WinHttpSendRequest()失败,GetLastError()返回ERROR_WINHTTP_SECURE_FAILURE (12057)

原因:目标设备 Web 服务使用自签名证书,而 WinHTTP 默认启用证书链验证。WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, ...)未正确设置。
解决:在WinHttpOpenRequest()后、WinHttpSendRequest()前插入:

DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_CERT_CN_INVALID; WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));

4.3 现象:多线程并发抓取时,SetCookie()导致程序崩溃,Dr. Memory 报告HEAP CORRUPTION

原因:CRITICAL_SECTION未初始化。HttpClient构造函数中漏写了InitializeCriticalSection(&m_csCookie),导致EnterCriticalSection()访问野指针。
解决:在构造函数中显式初始化:

HttpClient::HttpClient(DWORD dwFlags) : m_hSession(NULL), m_hConnect(NULL) { InitializeCriticalSection(&m_csCookie); // ... 其他初始化 } // 并在析构函数中 DeleteCriticalSection(&m_csCookie)

4.4 现象:抓取http://192.168.1.100/返回乱码,但浏览器访问正常;response_body的前 10 字节是<!DOCTYPE html>,但中文全为??

原因:设备 Web 服务返回的 HTML 中<meta>标签缺失 charset 声明,且响应头Content-Type未带charset=gbk,HtmlExtractor::DetectEncoding()fallback 到CP_ACP(当前系统 ANSI 代码页),而工控机系统区域设为“英语(美国)”,CP_ACP=1252,无法解码 GBK 字节。
解决:在ConvertToUtf8()函数开头硬编码优先尝试 GBK:

if (response_body.size() >= 2 && (response_body[0] == '\xC0' || response_body[0] == '\xC1')) { // GBK 常见首字节范围,直接走 GbkToUtf8 return GbkToUtf8(response_body, out_utf8); }

4.5 现象:程序在 Windows Server 2012 R2 上启动时报错0xC0000005: Access violation reading location 0x00000000,堆栈指向WinHttpCloseHandle(NULL)

原因:m_hSession或m_hConnect在异常路径下未置 NULL,析构函数中再次调用WinHttpCloseHandle()传入野指针。
解决:所有WinHttpCloseHandle()调用前加空指针检查,并在关闭后立即将句柄置 NULL:

if (m_hConnect) { WinHttpCloseHandle(m_hConnect); m_hConnect = NULL; }

注意:WinHTTP 句柄是 Windows 内核对象句柄,CloseHandle()后再用会触发严重错误,必须置 NULL 并在所有路径保证只 close 一次。


5. 二次开发实战:如何把抓取模块集成进你的 MFC 对话框程序

现在你已经跑通了命令行示例,下一步是把它变成你现有项目的“插件”。我以最常见的场景为例:你在用 MFC 开发一个设备配置工具,主界面是CDeviceConfigDlg,点击“刷新状态”按钮后,需要异步抓取 5 个 IP 地址的/status.json,解析后填入列表控件CListCtrl m_listStatus。这里不讲 MFC 基础,只聚焦「怎么让 C++ 多线程抓取和 MFC UI 线程安全交互」。

5.1 关键改造点一:用 PostMessage 替代跨线程直接调用

MFC 的CWnd派生类(如CDialog)只能由创建它的线程(UI 线程)调用UpdateData()、SetItemText()等方法。若在线程池中直接调用m_listStatus.InsertItem(),会触发断言失败或静默崩溃。正确做法是定义自定义消息,在工作线程中PostMessage(),由 UI 线程的OnMyUpdateStatus()处理。

首先,在DeviceConfigDlg.h中定义消息:

// 在 class CDeviceConfigDlg 声明前 #define WM_UPDATE_STATUS (WM_USER + 1001) // 在 class CDeviceConfigDlg 内部 public: afx_msg LRESULT OnMyUpdateStatus(WPARAM wParam, LPARAM lParam);

然后在DeviceConfigDlg.cpp中实现:

BEGIN_MESSAGE_MAP(CDeviceConfigDlg, CDialogEx) ON_MESSAGE(WM_UPDATE_STATUS, &CDeviceConfigDlg::OnMyUpdateStatus) END_MESSAGE_MAP() LRESULT CDeviceConfigDlg::OnMyUpdateStatus(WPARAM wParam, LPARAM lParam) { // wParam 是设备索引(0~4),lParam 是 new char*[1024] 指向 UTF-8 字符串 int idx = static_cast<int>(wParam); char* pJson = reinterpret_cast<char*>(lParam); // 解析 JSON(此处用 cJSON 简化,你可用 rapidjson 或手写) cJSON* root = cJSON_Parse(pJson); if (root) { cJSON* temp = cJSON_GetObjectItem(root, "temperature"); if (temp && temp->valuestring) { CString strTemp(temp->valuestring); m_listStatus.SetItemText(idx, 1, strTemp); // 第二列显示温度 } cJSON_Delete(root); } delete[] pJson; // 释放工作线程分配的内存 return 0; }

5.2 关键改造点二:在工作线程中安全传递数据

HttpClient::AsyncGet()的回调函数运行在线程池线程,不能直接操作 UI。你需要在回调中new char[]分配内存,PostMessage()传递指针,再由 UI 线程负责delete[]。完整流程如下:

void CDeviceConfigDlg::OnBnClickedBtnRefresh() { // 启动 5 个异步抓取 for (int i = 0; i < 5; ++i) { std::wstring url = std::format(L"http://192.168.1.{}:8080/status.json", i + 100); m_httpClient.AsyncGet(url, [this, i](bool success, const std::string& body) { if (success && !body.empty()) { // 分配内存并拷贝 body(UTF-8) char* pCopy = new char[body.size() + 1]; memcpy(pCopy, body.data(), body.size()); pCopy[body.size()] = '\0'; // Post 到 UI 线程 ::PostMessage(m_hWnd, WM_UPDATE_STATUS, static_cast<WPARAM>(i), reinterpret_cast<LPARAM>(pCopy)); } }); } }

参数说明:

  • std::format()是 C++20,若你用 VS2015,改用_itow_s()+std::wstring拼接;
  • pCopy必须new char[],不能用std::string::c_str(),因为body在回调结束时析构;
  • PostMessage()是线程安全的,比SendMessage()更合适(不阻塞工作线程);
  • UI 线程收到消息后,必须delete[] pJson,否则内存泄漏。

5.3 关键改造点三:处理 MFC 程序退出时的资源清理

MFC 对话框关闭时,线程池可能还在运行。若不等待,HttpClient析构时WinHttpCloseHandle()可能被并发调用,导致崩溃。解决方案是在CDeviceConfigDlg::OnCancel()中加入同步等待:

void CDeviceConfigDlg::OnCancel() { // 先通知线程池停止接收新任务 m_threadPool.Stop(); // 假设 ThreadPool 有 Stop() 方法 // 等待所有进行中的抓取完成(最多等 5 秒) m_threadPool.WaitForAllTasks(5000); CDialogEx::OnCancel(); }

ThreadPool::WaitForAllTasks()的实现要点是维护一个原子计数器std::atomic<int> m_activeTasks,每次enqueue()前++m_activeTasks,任务结束时--m_activeTasks,WaitForAllTasks()循环Sleep(10)直到m_activeTasks == 0或超时。

从那以后我每次给 MFC 项目加网络模块,都强制走一遍「PostMessage 传递数据 + UI 线程 delete + 退出时 WaitForAllTasks」三步检查清单。不是怕写错,而是怕三年后接手的人在凌晨两点对着0xC0000005错误抓狂时,翻不到一句有用的注释。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询