最常见崩溃是多线程并发操作同一socket fd:一端close后另一端仍send/recv,引发Bad file descriptor或段错误;根本原因是资源生命周期失控,需用引用计数封装fd、严格限定close所有权,并避免在signal handler中调用非异步信号安全函数。

socket 文件描述符被多线程同时 close 导致崩溃
最常见的情况是:一个线程刚调用 close(fd),另一个线程还在用这个 fd 调用 send() 或 recv(),触发 Bad file descriptor 错误,甚至段错误。这不是锁能“修复”的问题,而是资源生命周期管理失控。
调试建议:
- 在所有
close()前加日志,打印线程 ID 和fd值; - 用
strace -f -e trace=close,send,recv,write,read观察哪个线程在何时操作了哪个fd; - 避免跨线程共享裸
int fd,改用带引用计数的封装(如std::shared_ptr<sockethandle></sockethandle>),close()放在析构里; - 如果必须共享,确保
close()仅由唯一所有者调用,其他线程只读/只写且不负责释放。
多个线程并发调用 send/recv 同一个 socket
POSIX 允许对同一个 fd 并发调用 send() 和 recv(),但行为不可控:数据可能交错、send() 返回值难解释、EINTR/EAGAIN 处理逻辑混乱。这不是线程安全问题,而是语义冲突。
加锁不能解决根本问题,但可让行为可预测:
立即学习“C++免费学习笔记(深入)”;
- 用
std::mutex保护整个发送流程(从准备缓冲区到send()返回); - 不要只锁
send()调用本身——如果发送前要拼包、计算校验和,这些也得进临界区; - 接收端同理:
recv()+ 解包 + 状态更新 必须原子; - 注意锁粒度:粗粒度锁(如 per-socket mutex)易成瓶颈;细粒度(如 per-message queue)需配合无锁队列或条件变量。
std::mutex 在 signal handler 里被 lock 导致死锁
如果 socket I/O 被封装在信号驱动(如 sigio)或 epoll_wait 配合 signalfd 使用,而你在 signal handler 里试图 lock std::mutex,会直接触发未定义行为——std::mutex::lock() 不是 async-signal-safe 函数。
正确做法:
- signal handler 只做最简动作:写一个字节到
signalfd对应的 pipe,或设置volatile sig_atomic_t标志; - 把 socket 操作移回主循环或专用 I/O 线程;
- 用
pthread_sigmask()阻塞信号,在 I/O 线程中用sigtimedwait()同步等待; - 绝对不要在 signal handler 中调用
send()、printf()、malloc()或任何 C++ RAII 构造。
gdb 调试时发现死锁却看不到谁持锁
gdb attach 后用 info threads 和 thread apply all bt 只能看到线程卡在 pthread_mutex_lock,但不知道哪个线程持有该锁。因为 std::mutex 底层是 pthread_mutex_t,GDB 默认不显示 owner 字段。
临时绕过方法:
- 编译时加
-D_GLIBCXX_DEBUG(GCC)启用 libstdc++ 调试模式,部分版本会增强锁诊断; - 改用
std::recursive_mutex并配合自定义 wrapper 记录 owner tid(需手动维护); - 更可靠的是用
libpthread的pthread_mutex_consistent()+PTHREAD_MUTEX_ROBUST属性(仅 Linux),配合pthread_mutex_trylock()探测; - 生产环境优先用
perf record -e sched:sched_mutex_lock,sched:sched_mutex_unlock抓取锁事件流。
真正麻烦的从来不是“怎么加锁”,而是“哪些状态需要锁”和“锁的边界是否覆盖了所有竞态路径”。比如 socket 的超时设置(setsockopt(SO_RCVTIMEO))、非阻塞标志(fcntl(fd, F_SETFL, O_NONBLOCK))这些元操作,一旦被多个线程反复修改,也会引发难以复现的 I/O 行为异常——它们同样需要保护,但常被忽略。


















