跳转到正文
zeno's blog
返回

asio(六):十大陷阱与生产实践

专题: Asio

Table of contents

Open Table of contents

TL;DR

Asio 最常见的 bug 源头是对象生命周期(异步操作引用了已析构的对象)和缓冲区生命周期(buffer 底层内存在操作完成前被释放)。strand 不是锁但胜似锁——用错了比不用更危险。timer 取消的语义和直觉不一致。这篇列出 10 个陷阱和 7 条生产 checklist。


陷阱

1. 对象生命周期悬空引用(最常见的 bug)

症状:段错误、随机崩溃、内存损坏,通常在高负载时偶发。

原因:异步操作的 handler 引用了一个已经被析构的对象。

// ❌ 错误:session 可能在 handler 执行前被析构
class session {
    tcp::socket socket_;
    std::array<char, 1024> buf_;

    void start() {
        socket_.async_read_some(asio::buffer(buf_),
            [this](auto ec, auto n) {
                // this 可能已经是野指针
                process(n);
            });
    }
};

修复enable_shared_from_this 模式,让 handler 持有 shared_ptr 保活对象。

// ✓ 正确:shared_ptr 保证对象活到 handler 执行完
class session : public std::enable_shared_from_this<session> {
    void start() {
        auto self = shared_from_this();
        socket_.async_read_some(asio::buffer(buf_),
            [self](auto ec, auto n) {
                self->process(n);  // self 持有 shared_ptr,对象一定存活
            });
    }
};

2. 缓冲区生命周期问题

症状:读到垃圾数据、段错误。

// ❌ 错误:msg 在函数返回时析构,buffer 悬空
void send(tcp::socket& socket) {
    std::string msg = "hello";
    async_write(socket, asio::buffer(msg), handler);
    // msg 被析构,但 async_write 还没完成
}

asio::buffer() 创建的是非拥有式引用——它只存储指针和长度,不持有内存。

// ✓ 正确:用 shared_ptr 延长 msg 的生命周期
void send(tcp::socket& socket) {
    auto msg = std::make_shared<std::string>("hello");
    async_write(socket, asio::buffer(*msg),
        [msg](auto ec, auto n) {
            // msg 的 shared_ptr 在 handler 中保活
        });
}

3. 忘记调用 io_context::run()

症状:程序不崩溃、不报错,就是什么都不做。

asio::io_context io;
tcp::socket socket(io);
socket.async_connect(endpoint, handler);
// 忘记 io.run()
// handler 永远不会被调用,程序静默退出

所有异步操作的 handler 只会在调用 run() 的线程上执行。没有 run(),handler 永远在队列里。

4. strand 误用导致竞争条件

症状:数据竞争、乱序执行,但只在多线程 run() 时出现。

asio::strand<asio::io_context::executor_type> strand(io.get_executor());
tcp::socket socket(io);  // ← socket 没有绑定 strand

// ❌ 这两个 handler 没有通过 strand 分发
socket.async_read_some(buf, read_handler);   // 可能在线程 A 执行
socket.async_write_some(buf, write_handler); // 可能在线程 B 同时执行
// ✓ 正确:通过 strand 绑定 handler
socket.async_read_some(buf,
    asio::bind_executor(strand, read_handler));
socket.async_write_some(buf,
    asio::bind_executor(strand, write_handler));

// 或者更好:从一开始就用 strand 构造 socket
tcp::socket socket(strand);

其他常见错误

5. timer 取消的反直觉语义

场景 1:取消一个已到期但 handler 还在队列中的 timer。

asio::steady_timer timer(io, std::chrono::seconds(1));
timer.async_wait(handler);

// 1 秒后 timer 到期,handler 被入队到完成队列
// 此时调用 cancel()...
timer.cancel();  // 没有效果!handler 已经在队列中,会正常执行

cancel() 只影响正在等待的操作,不影响已经完成但尚未分发的 handler。

场景 2:修改 timer 到期时间隐式取消。

timer.expires_after(std::chrono::seconds(5));
timer.async_wait(handler_A);

// 修改到期时间 → handler_A 会被调用,ec 为 operation_aborted
timer.expires_after(std::chrono::seconds(10));
timer.async_wait(handler_B);

场景 3:timer 析构时 handler 仍被调用。

{
    asio::steady_timer timer(io, std::chrono::seconds(5));
    timer.async_wait([&timer](auto ec) {
        // ❌ timer 已经析构了,这里访问 timer 是 UB
    });
}  // timer 析构,handler 会被调用(ec = operation_aborted),但 timer 已不存在

6. 异常从 handler 中传播

io.run();  // handler 抛异常会从这里传出来

如果一个 handler 抛异常:

陷阱:以为一个 handler 的异常会停止整个事件循环。实际上只影响一个线程。

7. DNS resolver 的隐形问题

8. Handler 分配开销

每个异步操作可能为 handler 分配内存(::operator new)。在高频操作(如每秒数十万次 I/O)时,这是可测量的性能瓶颈。

// Asio 的保证:handler 的内存在调用前被释放
// 这意味着可以实现一个简单的回收分配器

class handler_memory {
    std::aligned_storage_t<1024> storage_;
    bool in_use_ = false;

public:
    void* allocate(std::size_t size) {
        if (!in_use_ && size <= sizeof(storage_)) {
            in_use_ = true;
            return &storage_;
        }
        return ::operator new(size);
    }

    void deallocate(void* p) {
        if (p == &storage_) {
            in_use_ = false;
        } else {
            ::operator delete(p);
        }
    }
};

这之所以可行,是因为 Asio 保证「释放旧 handler 内存 → 调用 handler → handler 内发起新操作 → 分配新 handler 内存」这个顺序。旧内存在新分配时已经被释放了。

9. Work Guard 管理不当

auto work = asio::make_work_guard(io);
io.run();  // 永远不会返回!
// 忘记 work.reset(),无法优雅关闭

或者反过来:

// 没有 work guard
asio::steady_timer timer(io, std::chrono::seconds(60));
timer.async_wait(handler);
// 如果在 timer 到期前没有其他工作...
// run() 不会提前退出(timer 本身算 work),但如果 timer handler
// 内部又要发起新的 timer 等待,handler 执行和新 timer 注册之间
// 有微小的间隙,run() 可能在间隙中返回

10. 在 run() 还在运行时析构 io_context

asio::io_context io;
std::thread t([&] { io.run(); });

// ❌ 主线程退出时 io 被析构,但 t 还在调用 run()
// UB:析构非线程安全
// ✓ 正确:先停止,再 join,最后让 io 自然析构
io.stop();
t.join();
// io 在这之后析构是安全的

生产实践

1. 线程池大小

// 简单的线程池模式
asio::io_context io;
auto work = asio::make_work_guard(io);

unsigned n = std::thread::hardware_concurrency();
std::vector<std::thread> threads;
for (unsigned i = 0; i < n; ++i) {
    threads.emplace_back([&io] { io.run(); });
}

// 关闭
work.reset();
for (auto& t : threads) t.join();

asio::thread_pool 是内置的替代方案(默认 hardware_concurrency() * 2 个线程)。

2. 连接管理模式

class server {
    tcp::acceptor acceptor_;

    asio::awaitable<void> accept_loop() {
        for (;;) {
            auto socket = co_await acceptor_.async_accept(asio::use_awaitable);
            // 每个连接一个协程,自动管理生命周期
            asio::co_spawn(acceptor_.get_executor(),
                handle_connection(std::move(socket)),
                asio::detached);
        }
    }

    asio::awaitable<void> handle_connection(tcp::socket socket) {
        auto strand = asio::make_strand(socket.get_executor());
        // strand 保证这个连接的所有操作序列化
        // 连接关闭时协程自然结束,资源自动释放
    }
};

3. 错误处理策略

错误类型处理方式
asio::error::operation_aborted正常取消,不记录错误
asio::error::eof对端正常关闭,清理连接
asio::error::connection_reset对端异常关闭,清理连接
asio::error::would_block内部处理,用户不应看到
其他系统错误记录日志,视情况重试或关闭连接
// C++20 协程中推荐用 as_tuple 避免异常
auto [ec, n] = co_await socket.async_read_some(
    asio::buffer(buf), asio::as_tuple(asio::use_awaitable));

if (ec == asio::error::eof) {
    // 正常关闭
    co_return;
}
if (ec) {
    // 真正的错误
    log_error(ec);
    co_return;
}

4. 优雅关闭

asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](auto ec, int sig) {
    // 1. 停止接受新连接
    acceptor.close();

    // 2. 取消所有活跃连接的操作
    for (auto& conn : active_connections) {
        conn->cancel();
    }

    // 3. 释放 work guard
    work.reset();

    // 4. io_context::run() 会在所有 handler 执行完后自然返回
});

5. 超时控制

asio::awaitable<void> read_with_timeout(tcp::socket& socket,
                                         asio::mutable_buffer buf,
                                         std::chrono::seconds timeout) {
    asio::steady_timer timer(socket.get_executor(), timeout);

    // 同时等待读取和超时
    using namespace asio::experimental::awaitable_operators;

    auto result = co_await (
        socket.async_read_some(buf, asio::use_awaitable)
        || timer.async_wait(asio::use_awaitable)
    );

    // variant index 0 = read 先完成, 1 = timer 先完成
}

6. 内存管理

7. 调试工具

定义 BOOST_ASIO_ENABLE_HANDLER_TRACKING 启用 handler 追踪:

输出到 stderr 的格式:
@asio|1589487039.741377|0*1|socket@0x7fff...|async_read_some
@asio|1589487039.742128|>1|                 |
@asio|1589487039.742198|1*2|socket@0x7fff...|async_write

各字段含义:
- 0*1 = handler 0 创建了 handler 1
- >1  = handler 1 开始执行
- <1  = handler 1 执行结束

用 Asio 自带的 handlerviz.pl + GraphViz 可视化 handler 链:

# 需验证:工具是否仍在当前版本的 Asio 中提供
cat trace.log | perl handlerviz.pl | dot -Tpng > handlers.png

BOOST_ASIO_HANDLER_LOCATION 宏在追踪输出中嵌入源码位置信息。


Per-Operation Cancellation(Boost 1.77+)

细粒度取消,不需要关闭整个 socket:

asio::cancellation_signal cancel_signal;

socket.async_read_some(asio::buffer(buf),
    asio::bind_cancellation_slot(cancel_signal.slot(),
        [](auto ec, auto n) {
            if (ec == asio::error::operation_aborted) {
                // 被取消了
            }
        }));

// 在其他地方取消这个特定操作
cancel_signal.emit(asio::cancellation_type::total);
取消类型含义副作用
terminal取消后 I/O 对象只能 close/destroy
partial可能有部分副作用报告已传输字节数
total零可观察副作用

分享这篇文章:

上一篇
Go 并发(一):Channel 内部机制与使用模式
下一篇
asio(五):操作系统I/O多路复用-epoll、kqueue、IOCP如何被统一