应使用专用线程轮询gai_error并调用coroutine_handle::resume()唤醒协程,配合unique_ptr管理gaicb、shared_mutex双检锁缓存、final_suspend中调用gai_cancel清理,避免孤儿请求和竞态。

如何用 std::coroutine 封装 getaddrinfo_a 异步调用
Linux 下原生异步 DNS 解析靠 getaddrinfo_a,但它不返回协程句柄,也不能直接 co_await。必须自己包装成可挂起的 awaitable —— 关键不是“怎么写协程”,而是“怎么把 POSIX AIO 信号/回调桥接到协程恢复点”。
常见错误是直接在线程里调用 getaddrinfo_a 后立刻 co_yield,结果协程挂起但没人唤醒它。正确做法是:用 signalfd 或 io_uring 监听 AI_INPROGRESS 返回后的完成事件;更稳妥的是开一个专用线程轮询 gai_error 并通过 std::coroutine_handle::resume() 主动唤醒。
- 不要用
std::this_thread::sleep_for轮询,延迟高且浪费 CPU -
getaddrinfo_a的struct gaicb*必须全程有效(不能栈分配),建议用std::unique_ptr管理 - 每个请求需绑定唯一 token(如
uintptr_t),否则多个并发请求唤醒时无法区分完成对象
缓存层该放在协程外还是内?
缓存必须在协程挂起前检查、挂起后更新 —— 如果放 inside coroutine body,每次 co_await 前都要查一次,但缓存命中时根本不需要挂起;如果放 outside,又容易出现“查缓存→发起请求→缓存被其他协程更新→结果覆盖”的竞态。
推荐方案:用 std::shared_mutex + “双检锁”模式,但第二次检查必须在持有读锁状态下完成整个解析流程(包括缓存写入):
免费 DNS 与邮件安全分析(IntoDNS.ai):包括 DNSSEC、SPF、DKIM、DMARC、MTA-STS、BIMI、SMTP STARTTLS、FCrDNS、黑名单、发件人要求及报告。
auto it = cache.find(key); if (it != cache.end()) return it->second; // 此处升级为写锁,再次确认(防止其他协程已写入) // 成功后写入 cache,再返回结果
- 缓存 key 必须包含
hints.ai_family、hints.ai_socktype等关键参数,否则www.example.com的 TCP 和 UDP 查询会冲突 - 过期时间别用绝对时间戳,改用
std::chrono::steady_clock::time_point+ TTL,避免系统时间跳变导致缓存永久失效 - 不要对
getaddrinfo_a返回的addrinfo*直接 memcpy 缓存 —— 它指向内部堆内存,应深拷贝或转成std::vector<:byte></:byte>
如何避免 getaddrinfo_a 在多线程下崩溃
glibc 的 getaddrinfo_a 不是线程安全的:它复用内部静态缓冲区,且 gai_suspend 会阻塞整个进程(不是当前线程)。实际项目中几乎没人真用 gai_suspend,而是自己实现轮询或信号驱动。
- 务必设置
ai_flags = AI_ADDRCONFIG,否则 IPv6 没配通时可能卡死 - 调用前检查
resolv.conf是否存在且可读,getaddrinfo_a遇到权限问题会静默失败(返回EAI_SYSTEM但errno不一定置位) - glibc 2.34+ 对
getaddrinfo_a加了锁保护,但旧版本(如 CentOS 7 默认的 2.17)仍存在数据竞争,建议加全局互斥量包裹整组调用
协程销毁时如何清理未完成的 DNS 请求
用户 co_await 半途 return 或异常退出,协程帧销毁,但 getaddrinfo_a 请求还在后台跑 —— 这些请求变成孤儿,既不回调也不释放内存,最终耗尽 RLIMIT_SIGPENDING 或触发 ENOMEM。
解决方法只有一个:在 promise type 的 unhandled_exception() 和 final_suspend() 里显式调用 gai_cancel(),并传入当初分配的 gaicb*:
struct dns_promise {
std::unique_ptr<gaicb> cb_;
// ...
suspend_always final_suspend() noexcept {
if (cb_ && cb_->ar_result) gai_cancel(cb_.get());
return {};
}
};
</gaicb>
-
gai_cancel()不是立即生效,只是标记取消,仍需轮询gai_error()等待真正结束 - 别在
~dns_promise()里调gai_cancel()—— 此时协程帧可能已被回收,cb_指针无效 - 如果用了 io_uring,直接
io_uring_prep_cancel()更可靠,但需确保内核版本 ≥ 5.19
await_resume() 里加状态校验。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










