用std::coroutine封装getaddrinfo_a需轮询gai_error实现awaitable,堆分配gaicb;DNS缓存用带TTL的DnsEntry+后台清理线程(shared_mutex保护);并发请求通过inflight promise共享结果;清理线程须显式控制启停以防析构问题。

如何用 std::coroutine 封装 getaddrinfo_a 异步调用
Linux 下原生异步 DNS 解析靠 getaddrinfo_a,但它不返回协程句柄,也不能直接 co_await。必须自己封装成可挂起的 awaitable —— 关键是把 struct gaicb* 和信号(SIGIO)或轮询(gai_error)桥接到协程恢复逻辑上。
推荐用轮询方式(更可控、无信号上下文风险),在 await_ready 中检查 gai_error(cb) 是否为 EAI_INPROGRESS,不是就立即完成;否则在 await_suspend 里启动一个轻量级轮询任务(比如用 std::thread + std::this_thread::sleep_for,或投递到线程池),完成后调用 coroutine_handle::resume()。
-
gaicb必须堆分配(栈变量在挂起后失效) - 轮询间隔建议 ≥10ms,太密浪费 CPU,太长响应延迟高
- 不要在
await_suspend里直接阻塞等待,否则协程调度器线程会被卡住
如何让 DNS 缓存自动过期并触发清理
协程本身不管理生命周期,缓存过期必须靠外部机制。典型做法是:每个解析结果存为 struct DnsEntry { std::string hostname; std::vector<sockaddr_storage> addrs; std::chrono::steady_clock::time_point expires; }</sockaddr_storage>,然后用一个后台 std::thread 定期扫描容器(如 std::unordered_map<:string dnsentry></:string>),删掉 expires < now() 的项。
注意:这个清理线程和协程可能并发访问缓存容器,必须加锁(std::shared_mutex 更合适,读多写少);如果使用 boost::lockfree::queue 或无锁结构,反而容易因 ABA 问题导致过期条目漏删。
立即学习“C++免费学习笔记(深入)”;
- 过期时间建议从 DNS 响应的 TTL 字段提取(
getaddrinfo_a不提供,需改用libresolv或ldns底层解析) - 若只用
getaddrinfo_a,TTL 只能硬编码(如 30s),不能反映真实权威记录 - 清理线程休眠时间应 ≤ 最小 TTL / 2,避免大量过期项堆积
如何避免协程重复发起相同域名请求
并发场景下,多个协程同时查同一域名,不该发多次系统调用。需要一个“正在解析中”的登记表,配合 std::promise/std::future 或 std::shared_ptr<std::promise<...>> 共享结果。
典型结构:std::unordered_map<std::string, std::shared_ptr<std::promise<std::vector<sockaddr_storage>>>> inflight;。首次请求插入 promise;后续同域名请求直接 co_await future;getaddrinfo_a 完成后 set_value,所有等待者被唤醒。
- 登记表也要加锁(
std::shared_mutex),但只在 insert/lookup 时短暂持有 - promise 的 lifetime 必须延续到所有协程 resume 完成,用
shared_ptr自动管理最稳妥 - 注意异常路径:如果
getaddrinfo_a返回失败,要调用promise->set_exception(),否则等待协程永远挂起
为什么不用 std::jthread 管理清理线程
std::jthread 确实能自动 join,但 DNS 清理线程必须确保在所有协程结束、缓存容器析构前退出,否则访问已销毁内存。而协程可能跨多个作用域存活(比如被存进某个 manager 对象),析构顺序不可控。
更安全的做法是显式控制:定义一个全局或单例 DnsCacheManager,在其析构函数里调用 stop_requested_.store(true) 并 cv_.notify_all(),然后 join()。清理线程循环里始终检查 stop_requested_,且每次 wait 带超时,防止永久阻塞。
- 别依赖 RAII 自动 stop,C++ 没有协程作用域终结通知机制
- 用
std::condition_variable而非sleep_for主动轮询,降低唤醒延迟 - 析构时若清理线程还没退出,
join()可能死锁 —— 必须保证它收到通知后能快速退出


















