std::ofstream赋值运算符仅在GDB调试时触发段错误问题排查
多线程日志程序GDB调试下段错误问题分析与解决
问题描述
我编写了一个多线程程序,每个线程独立打开文本文件用于调试日志记录,每个线程由独立的Log类实例管理,该实例负责维护std::thread对象、std::ofstream对象以及线程的主执行循环。调试时发现:仅在GDB调试器激活,且线程数量超过系统CPU核心数时,打开日志文件会触发段错误。
简化示例代码
#include <chrono> #include <fstream> #include <memory> #include <thread> #include <vector> class Log final{ private: int i_; std::thread worker_; std::ofstream log_; public: Log(int i) : i_(i), worker_(&Log::doWork, this) {}; ~Log(){ worker_.join(); } private: void doWork(){ log_ = std::ofstream( std::to_string(i_) + "_" + std::to_string(std::hash<std::thread::id>{}(std::this_thread::get_id())) + ".log" ); for (int i = 0 ; i < 999999 ; ++i){ log_ << i << '\n'; } log_.close(); } }; int main (void){ std::vector<std::unique_ptr<Log>> vec; for (int i = 0 ; i < 50 ; ++i){ auto l = std::make_unique<Log>(i); vec.emplace_back(std::move(l)); } return 0; }
编译环境与选项
使用GCC 11.4.0编译,编译选项:
-std=c++17 -g
GDB回溯信息
#0 0x00007ffff7e94f17 in std::basic_ofstream<char, std::char_traits<char> >::operator=(std::basic_ofstream<char, std::char_traits<char> >&&) () from /lib/x86_64-linux-gnu/libstdc++.so.6 #1 0x0000555555556d07 in Log::doWork (this=0x555555571b40) at log.cc:30 #2 0x0000555555558d4f in std::__invoke_impl<void, void (Log::*)(), Log*> ( __f=@0x555555571d70: (void (Log::*)(Log * const)) 0x555555556bf2 <Log::doWork()>, __t=@0x555555571d68: 0x555555571b40) at /usr/include/c++/11/bits/invoke.h:74 #3 0x0000555555558cd1 in std::__invoke<void (Log::*)(), Log*> ( __fn=@0x555555571d70: (void (Log::*)(Log * const)) 0x555555556bf2 <Log::doWork()>) at /usr/include/c++/11/bits/invoke.h:96 #4 0x0000555555558c31 in std::thread::_Invoker<std::tuple<void (Log::*)(), Log*> >::_M_invoke<0ul, 1ul> ( this=0x555555571d68) at /usr/include/c++/11/bits/std_thread.h:259 #5 0x0000555555558be6 in std::thread::_Invoker<std::tuple<void (Log::*)(), Log*> >::operator() (this=0x555555571d68) at /usr/include/c++/11/bits/std_thread.h:266 #6 0x0000555555558bc6 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (Log::*)(), Log*> > >::_M_run (this=0x555555571d60) at /usr/include/c++/11/bits/std_thread.h:211 #7 0x00007ffff7e58253 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6 #8 0x00007ffff7bc7ac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442 #9 0x00007ffff7c59850 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81
问题原因
核心问题是类成员初始化顺序错误:
- C++类成员的初始化顺序由其在类中的声明顺序决定,而非构造函数初始化列表的顺序。
Log类中worker_声明在log_之前,因此构造函数会先初始化worker_,启动线程执行doWork方法。 - 此时
log_尚未完成默认构造(std::ofstream的默认构造仅做浅初始化,内部文件句柄等状态未就绪),而在GDB调试+线程数超核心数的场景下,线程调度更频繁,worker线程会比log_的默认构造更早执行到log_ = std::ofstream(...)这一行。 - 对未完全初始化的
std::ofstream对象执行移动赋值操作,属于未定义行为,最终触发段错误。 - 将日志打开逻辑移至初始化列表能解决问题,是因为调整
log_的声明顺序后,log_先完成默认构造,worker线程启动时log_已处于合法状态。
可行解决方案
方案1:延迟线程启动,确保成员初始化完成
调整类成员声明顺序,让log_先完成初始化,再在构造函数体内启动线程,避免线程访问未初始化的成员:
#include <chrono> #include <fstream> #include <memory> #include <thread> #include <vector> class Log final{ private: int i_; std::ofstream log_; // 调整顺序,先初始化log_ std::thread worker_; // 后初始化worker_ public: Log(int i) : i_(i), log_() { worker_ = std::thread(&Log::doWork, this); // 构造函数体内启动线程,所有成员已初始化 } ~Log(){ worker_.join(); } private: void doWork(){ log_.open( std::to_string(i_) + "_" + std::to_string(std::hash<std::thread::id>{}(std::this_thread::get_id())) + ".log" ); for (int i = 0 ; i < 999999 ; ++i){ log_ << i << '\n'; } log_.close(); } }; int main (void){ std::vector<std::unique_ptr<Log>> vec; for (int i = 0 ; i < 50 ; ++i){ auto l = std::make_unique<Log>(i); vec.emplace_back(std::move(l)); } return 0; }
方案2:将log_改为线程内局部变量
直接在doWork中创建局部的std::ofstream对象,完全规避成员初始化顺序问题:
#include <chrono> #include <fstream> #include <memory> #include <thread> #include <vector> class Log final{ private: int i_; std::thread worker_; public: Log(int i) : i_(i), worker_(&Log::doWork, this) {}; ~Log(){ worker_.join(); } private: void doWork(){ std::ofstream log( std::to_string(i_) + "_" + std::to_string(std::hash<std::thread::id>{}(std::this_thread::get_id())) + ".log" ); for (int i = 0 ; i < 999999 ; ++i){ log << i << '\n'; } // 局部对象销毁时自动关闭文件,无需手动调用close } }; int main (void){ std::vector<std::unique_ptr<Log>> vec; for (int i = 0 ; i < 50 ; ++i){ auto l = std::make_unique<Log>(i); vec.emplace_back(std::move(l)); } return 0; }
方案3:用std::optional延迟初始化log_
使用std::optional包裹std::ofstream,确保在线程内调用emplace前,log_处于空状态,避免操作未初始化对象:
#include <chrono> #include <fstream> #include <memory> #include <optional> #include <thread> #include <vector> class Log final{ private: int i_; std::thread worker_; std::optional<std::ofstream> log_; public: Log(int i) : i_(i), worker_(&Log::doWork, this) {}; ~Log(){ worker_.join(); } private: void doWork(){ log_.emplace( std::to_string(i_) + "_" + std::to_string(std::hash<std::thread::id>{}(std::this_thread::get_id())) + ".log" ); for (int i = 0 ; i < 999999 ; ++i){ (*log_) << i << '\n'; } log_->close(); log_.reset(); } }; int main (void){ std::vector<std::unique_ptr<Log>> vec; for (int i = 0 ; i < 50 ; ++i){ auto l = std::make_unique<Log>(i); vec.emplace_back(std::move(l)); } return 0; }
内容的提问来源于stack exchange,提问作者user18
相关产品推荐
相关产品推荐

