本地C/S环境下1万并发TCP连接死锁问题排查
在这个简单实现中,服务器通过asio::io_context启动listener协程接受入站连接,每建立一个连接就启动一个session协程。同时启动10000个并发客户端与服务器建立连接,服务器会循环向每个客户端写入1KB的数据块。主线程等待所有连接建立完成后,休眠一段时间让服务器与客户端完成数据交互,随后将标志位stop设为true,预期session协程会退出并销毁底层套接字以终止客户端连接,但实际始终有部分连接无法关闭。
运行后卡在3047个客户端连接无法关闭,推测问题出在session协程挂起在async_write调用中从未恢复。按逻辑客户端应该持续读取数据释放缓冲区空间,让服务器完成写入后退出session、终止连接,为何会出现这种死锁场景?
代码实现
全局定义与客户端逻辑
using asio::ip::tcp; using Executor = asio::io_context::executor_type; std::atomic_bool stop = false; std::atomic_int32_t connected = 0; int numConnections = 10'000; namespace Client { asio::awaitable<void, Executor> client() try { const auto ex = co_await asio::this_coro::executor; tcp::socket socket{ex}; tcp::resolver res{ex}; co_await async_connect(socket, res.resolve("localhost", "12345"), asio::deferred); ++connected; for (std::vector<char> buf(1024);;) co_await async_read(socket, asio::mutable_buffer(buf.data(), buf.size()), asio::deferred); } catch (asio::system_error const &se) { --connected; } }
服务器逻辑
namespace Server { asio::awaitable<void, Executor> session(tcp::socket socket) try { for (static const std::vector payload(1024, 'A'); !stop;) { co_await async_write(socket, asio::buffer(payload), asio::as_tuple(asio::deferred)); } } catch (asio::system_error &err) { log() << err.code().message() << std::endl; } asio::awaitable<void, Executor> listener() try { const auto executor = co_await asio::this_coro::executor; for (tcp::acceptor acceptor{executor, {{}, 12345}};;) { co_spawn(executor, session(co_await acceptor.async_accept(asio::deferred)), asio::detached); } } catch (asio::system_error const &se) { log() << se.code().message() << std::endl; } }
主函数
using namespace std::chrono_literals; using std::this_thread::sleep_for; #include <ranges> int main() { asio::io_context ctx; asio::cancellation_signal signal; co_spawn(ctx, Server::listener(), asio::bind_cancellation_slot(signal.slot(), asio::detached)); std::ranges::for_each(std::views::iota(0, numConnections), [&](int) { co_spawn(ctx, Client::client(), asio::detached); }); std::jthread th([&ctx] { ctx.run(); }); while (connected < numConnections) { sleep_for(1s); log() << "Clients connected: " << connected << std::endl; } sleep_for(5s); stop = true; signal.emit(asio::cancellation_type::terminal); while (connected > 0) { sleep_for(1s); log() << "Clients still connected: " << connected << std::endl; } }
运行输出
main:74 Clients connected: 4097 main:74 Clients connected: 8107 main:74 Clients connected: 8107 main:74 Clients connected: 9174 main:74 Clients connected: 9174 client:25 All clients connected main:74 Clients connected: 10000 listener:53 Operation aborted. main:83 Clients still connected: 3047 main:83 Clients still connected: 3047 main:83 Clients still connected: 3047 main:83 Clients still connected: 3047 main:83 Clients still connected: 3047 main:83 Clients still connected: 3047 main:83 Clients still connected: 3047
问题分析与原因
核心死锁点
当stop被设为true时,部分session协程正挂起在async_write操作上,无法被唤醒检查stop标志,导致这些协程无法退出,套接字也不会被销毁,客户端的async_read会一直阻塞,connected计数无法递减。
具体原因
TCP流控与缓冲区积压
服务器持续向客户端写入数据,当客户端的TCP接收缓冲区被填满后,操作系统会停止向客户端发送数据,服务器的async_write会因为TCP窗口关闭而挂起。此时如果stop被设置,这些挂起的async_write不会主动取消,协程会一直等待TCP窗口重新打开,而客户端因为还在等待async_read完成,不会主动关闭连接,形成循环等待。缺乏取消机制
代码中仅通过stop标志让session协程在async_write完成后退出,但对于已经挂起的async_write操作,没有绑定取消信号。当stop设置后,这些挂起的写操作不会被中断,协程永远无法回到循环条件检查stop的逻辑。客户端的阻塞逻辑
客户端的async_read会一直等待数据,直到套接字被关闭或出错。如果服务器的session协程没有销毁套接字,客户端不会触发异常,connected计数不会减少。
解决方案建议
- 给
session协程的async_write绑定取消槽,当stop触发时主动取消所有挂起的写操作(需要将cancellation_signal传递到session协程中):// 修改session协程中的async_write调用 auto cancel_slot = asio::bind_cancellation_slot(signal.slot(), asio::deferred); co_await async_write(socket, asio::buffer(payload), asio::as_tuple(cancel_slot)); - 当
stop设置后,主动遍历并关闭所有服务器端套接字,强制终止连接。 - 客户端增加超时逻辑,长时间未收到数据时主动关闭连接。
内容的提问来源于stack exchange,提问作者Yannik Schrödder

