必须用std::mutex+std::lock_guard保护所有共享容器;libcurl需设curlopt_nosignal=1l、超时参数及curlopt_tcp_keepalive;std::thread须join();url去重推荐布隆过滤器+归一化。

std::thread 启动爬虫任务时,如何避免资源竞争和连接泄漏
直接用 std::thread 拉起多个 std::function 去抓 URL,最容易出问题的不是并发数,而是共享资源没加锁、HTTP 连接没显式关闭、异常路径没回收句柄。
典型表现:程序跑几分钟后卡住,netstat -an | grep :80 显示几百个 TIME_WAIT 或 ESTABLISHED;或者 std::vector<:thread></:thread> 析构时报 std::system_error: Resource deadlock avoided。
- 所有共享容器(如待抓队列、已抓 URL 集合、结果缓存)必须用
std::mutex+std::lock_guard保护,别依赖“我只读不写”这种假设 - 每个线程内使用
libcurl时,务必在每次请求后调用curl_easy_cleanup();若复用CURL*句柄,需确保同一句柄不跨线程使用 -
std::thread对象必须在析构前调用join()或detach()—— 更推荐join(),配合std::vector<:thread></:thread>的 RAII 管理
用 std::async + std::future 控制并发上限,比裸 thread 更稳
std::async 默认可能启动新线程,也可能延迟执行,但配合 std::launch::async 和线程池封装,能更可控地限制并发请求数。它天然解决 std::thread 的生命周期管理难题,也避免手动传参导致的引用悬空。
常见误用:auto f = std::async([]{ /* long IO */ }); 忘记取 f.get(),导致 future 析构时阻塞等待——这会让“异步”变成“同步串行”。
- 并发上限建议硬编码为常量(如
const int MAX_CONCURRENCY = 10),用std::queue<:future>></:future>缓存活跃任务 - 每次提交新任务前,先
wait_for(0s)检查已有 future 是否就绪,未就绪则 pop 掉已完成的,保持队列长度 ≤ MAX_CONCURRENCY - 别把
std::string_view或局部std::string直接捕获进 lambda:用值捕获或显式std::move,否则抓到的是野指针
libcurl 多线程下 CURLOPT_NOSIGNAL 必须设为 1L
libcurl 默认启用信号处理(比如超时时发 SIGALRM),但在多线程环境里,信号可能被错误线程接收,导致整个进程 crash 或挂起。这是 C++ 爬虫最隐蔽的崩溃源之一。
现象包括:偶发段错误、curl_easy_perform() 返回 CURLE_ABORTED_BY_CALLBACK 却没触发回调、或某次请求后整个线程卡死无响应。
- 每个
CURL*句柄初始化后立即设置:curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L) - 同时必须设置超时参数:
curl_easy_setopt(handle, CURLOPT_TIMEOUT, 30L)和curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, 10L),否则 NOSIGNAL 关闭后超时机制失效 - 禁用信号后,DNS 解析仍可能阻塞(尤其在 glibc 低版本),可额外加
curl_easy_setopt(handle, CURLOPT_DNS_CACHE_TIMEOUT, 60L)减少重复查询
URL 去重与调度不能只靠 std::set<:string>
单纯用 std::set<:string></:string> 存已访问 URL,内存涨得飞快,且无法处理语义等价(如 /a?b=1&c=2 和 /a?c=2&b=1)。更麻烦的是,多线程下 insert() + count() 不是原子操作,会漏判重复。
实际部署中,一个百万级种子 URL 的爬虫,std::set 内存占用常超 2GB,而布隆过滤器(Bloom Filter)加归一化处理能把内存压到 50MB 以内。
- URL 归一化至少做三件事:转小写、移除默认端口(
:80/:443)、排序并标准化 query 参数 - 去重逻辑必须封装成原子函数,例如
bool should_crawl(const std::string& url),内部用std::mutex保护布隆过滤器 + 小规模 LRU cache(用于近期高频碰撞检测) - 别用
std::unordered_set替代 —— 它的桶锁粒度太粗,高并发下性能反而不如加锁的std::set,而布隆过滤器无锁且 O(1)
实际跑起来你会发现,瓶颈从来不在并发模型本身,而在 DNS 解析阻塞、TCP 连接复用率低、SSL 握手开销,以及——没人告诉你 libcurl 的 CURLOPT_TCP_KEEPALIVE 默认是关的。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











