使用Asio async_write_some发送数据时服务偶发崩溃的原因排查
从你的崩溃调用栈和代码细节来看,这个偶发崩溃的核心原因是多线程环境下对共享数据结构的非线程安全访问,导致内存结构被破坏,最终触发_int_free阶段的堆错误。具体拆解如下:
1. 共享队列waitwriteq_的并发访问冲突
你的代码中waitwriteq_应该是普通std::queue(无线程安全保护),但它会被两类线程交叉操作:
- IO线程:在
AsyncWriteSomeCallback中执行waitwriteq_.front()和waitwriteq_.pop() - 业务线程:在
SendPacket中执行waitwriteq_.push()(从调用栈第16帧可推测,SendPacket会在发送中状态时将数据包入队)
std::queue本身不具备线程安全性,并发的push和pop会直接破坏队列内部的链表结构(比如节点指针被覆盖、内存重复释放等),后续内存分配/释放时就会触发非法访问,最终引发堆崩溃。
2. sendding_原子变量的竞态条件
虽然sendding_是std::atomic<bool>,但代码中存在检查-执行的竞态窗口:
// 推测SendPacket中的逻辑: if (!sendding_.load()) { AsyncSendData(packet); } else { waitwriteq_.push(packet); }
当多个业务线程同时执行这段代码时,可能出现多个线程都检测到sendding_为false,进而同时调用AsyncSendData,导致同一个socket发起多个async_write_some操作。这不仅会打乱发送顺序,还可能导致Buffer被重复处理,引发引用计数异常、内存重复释放等问题。
3. 修复建议
针对以上问题,需要给共享资源添加线程安全保护,并修复竞态条件:
(1)给共享资源加互斥锁
在AsioChannel类中添加std::mutex,保护所有对waitwriteq_和sendding_的访问:
// 类成员添加互斥锁 std::mutex send_mutex_; // 修改AsyncSendData void AsioChannel::AsyncSendData(const std::shared_ptr<Buffer>& packet) { std::lock_guard<std::mutex> lock(send_mutex_); sendding_.store(true); auto self = shared_from_this(); socket_.async_write_some(asio::buffer(packet->peek(), packet->readableBytes()), [self, packet](const std::error_code& err, std::size_t writen) { std::lock_guard<std::mutex> lock(self->send_mutex_); self->AsyncWriteSomeCallback(err, packet, writen); }); } // 修改AsyncWriteSomeCallback的队列操作 void AsioChannel::AsyncWriteSomeCallback(const std::error_code& error, const std::shared_ptr<Buffer>& packet, std::size_t writen) { if (error) { sendding_.store(false); return; } if (writen < packet->readableBytes()) { packet->retrieve(writen); AsyncSendData(packet); } else { if(!waitwriteq_.empty()) { auto next = waitwriteq_.front(); waitwriteq_.pop(); AsyncSendData(next); } else { sendding_.store(false); } } } // 修改SendPacket的入队逻辑 void AsioChannel::SendPacket(const std::shared_ptr<Buffer>& packet, size_t size) { std::lock_guard<std::mutex> lock(send_mutex_); if (sendding_.load()) { waitwriteq_.push(packet); } else { AsyncSendData(packet); } }
(2)确保IO操作的线程亲和性
业务线程不要直接操作socket和共享队列,而是通过asio::post将发送任务投递到IO线程执行,避免跨线程竞态:
void AsioChannel::SendPacket(const std::shared_ptr<Buffer>& packet, size_t size) { asio::post(socket_.get_executor(), [self = shared_from_this(), packet]() { std::lock_guard<std::mutex> lock(self->send_mutex_); if (self->sendding_.load()) { self->waitwriteq_.push(packet); } else { self->AsyncSendData(packet); } }); }
(3)测试阶段的内存检测
测试环境下可以开启GCC的地址 sanitizer:-fsanitize=address,它能精准定位内存错误的触发点,帮助你验证修复效果(注意不要在生产环境使用,会带来性能开销)。
内容的提问来源于stack exchange,提问作者Doraemon

