SocketCAN多线程场景下类析构函数提前调用原因排查
为什么基于SocketCAN的多线程程序启动后Canloop析构函数会立即被调用?
我正在排查一个问题:基于SocketCAN的多线程程序启动后,Canloop类的析构函数会立即被调用。我既未关闭程序也未显式销毁该类实例,本不应触发析构函数,但却得到了不符合预期的输出。
以下是程序代码片段:
#include <iostream> #include <string> #include <chrono> #include <thread> #include <mutex> #include <atomic> #include <cstring> #include <cstdlib> #include <unistd.h> #include <net/if.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <fcntl.h> #include <linux/can.h> #include <linux/can/raw.h> class Canloop { private: int socketFileDescriptor; struct sockaddr_can socketAdressConfig; struct ifreq interfaceRequestConfig; struct can_frame canFrameRx; struct can_frame canFrameTx; int sendCounter{0}; std::thread receive_; std::thread transmit_; std::mutex readWriteMutex; std::atomic<bool> running{true}; void receiveThread(); void transmitThread(); public: Canloop(std::string &canInterface); void setCanData(can_frame &); void start(); void stop(); ~Canloop(); }; Canloop::Canloop(std::string &canInterface) { if ((socketFileDescriptor = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) { perror("Socket"); } strcpy(interfaceRequestConfig.ifr_name, canInterface.c_str()); ioctl(socketFileDescriptor, SIOCGIFINDEX, &interfaceRequestConfig); memset(&socketAdressConfig, 0, sizeof(socketAdressConfig)); socketAdressConfig.can_family = AF_CAN; socketAdressConfig.can_ifindex = interfaceRequestConfig.ifr_ifindex; if (bind(socketFileDescriptor, (struct sockaddr *)&socketAdressConfig, sizeof(socketAdressConfig)) < 0) { perror("Bind"); } int flags = fcntl(socketFileDescriptor, F_GETFL, 0); if (flags == -1) { perror("fcntl non blocking set error"); } flags != O_NONBLOCK; if (fcntl(socketFileDescriptor, F_SETFL, flags) == -1) { perror("fcntl"); } int bufferSize = 2; if (setsockopt(socketFileDescriptor, SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize))) { perror("setsocketopt"); } if (setsockopt(socketFileDescriptor, SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize))) { perror("setsocketopt"); } struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 1000; if (setsockopt(socketFileDescriptor, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout))) { perror("time set error"); } if (setsockopt(socketFileDescriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))) { perror("time set error"); } } void Canloop::start() { receive_ = std::thread([=] { receiveThread(); }); transmit_ = std::thread([=] { transmitThread(); }); } void Canloop::setCanData(can_frame &data) { this->canFrameTx = data; } void Canloop::stop() { if (close(socketFileDescriptor) < 0) { perror("Close"); } running = false; if (receive_.joinable()) { receive_.join(); } if (transmit_.joinable()) { transmit_.join(); } } void Canloop::receiveThread() { std::cout << "Starting Rx_ " << std::endl; while (running) { ssize_t bytesRead; { // Mutex to guard the socket for Read access std::lock_guard<std::mutex> lock(readWriteMutex); bytesRead = read(socketFileDescriptor, &canFrameRx, sizeof(struct can_frame)); } if (running) { if (bytesRead == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // Handle non-blocking read } else { perror("read error"); } } else if (bytesRead == sizeof(struct can_frame)) { // Print data printf("0x%03X [%d] ", canFrameRx.can_id, canFrameRx.can_dlc); for (int i = 0; i < canFrameRx.can_dlc; i++) printf("%02X ", canFrameRx.data[i]); printf("\r\n"); } else { std::cout << "Incomplete data" << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } } void Canloop::transmitThread() { std::cout << "Starting Tx_" << std::endl; while (running) { ssize_t nbytes ; { // Lock for write std::lock_guard<std::mutex> lock(readWriteMutex); nbytes = write(socketFileDescriptor, &canFrameTx, sizeof(struct can_frame)); } if (running && nbytes == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { std::cout << "Would block" << std::endl; } else if (errno == ENOBUFS) { perror("Buffer fail: "); } else { perror("Write fail"); } } std::cout << "Data written " << sendCounter++ << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } Canloop::~Canloop() { std::cout << "Destructor called" << std::endl; if (transmit_.joinable()) { transmit_.join(); } if(receive_.joinable()) { receive_.join(); } } int main() { std::string canInterface("can0"); Canloop cantester(canInterface); can_frame localCanData; char hello[5] = "Hell"; localCanData.can_id = 0x123; localCanData.can_dlc = sizeof(hello) / sizeof(hello[0]); sprintf((char *)localCanData.data, hello); cantester.setCanData(localCanData); cantester.start(); return 0; }
程序运行输出:
Starting Rx_ Starting Tx_ Destructor called Data written 0 Data written 1 Data written 2 Data written 3 Data written 4 Data written 5 Data written 6 Data written 7 Data written 8 Data written 9
问题原因
核心问题出在main函数的执行流程:
Canloop cantester(canInterface);是创建在栈上的局部对象,生命周期仅限于main函数执行期间。- 调用
cantester.start()启动线程后,main直接执行return 0;,主线程立即结束。 - 主线程结束时,栈上的局部对象
cantester会被自动销毁,触发Canloop的析构函数。 - 此时两个工作线程的
running原子变量仍为true,线程会继续执行,因此出现析构函数调用后还能看到Data written输出的情况。
解决方案
方式一:让主线程等待,避免局部对象提前销毁
在main函数的return 0;之前添加等待逻辑,确保工作线程能持续运行:
int main() { std::string canInterface("can0"); Canloop cantester(canInterface); can_frame localCanData; char hello[5] = "Hell"; localCanData.can_id = 0x123; localCanData.can_dlc = sizeof(hello) / sizeof(hello[0]); sprintf((char *)localCanData.data, hello); cantester.setCanData(localCanData); cantester.start(); // 等待用户输入,防止主线程立即结束 std::cout << "Press enter to exit..." << std::endl; std::cin.get(); // 先停止线程再结束 cantester.stop(); return 0; }
方式二:调用stop方法,优雅终止线程
在main结束前调用cantester.stop(),该方法会设置running为false并等待线程结束,之后再销毁对象:
int main() { std::string canInterface("can0"); Canloop cantester(canInterface); can_frame localCanData; char hello[5] = "Hell"; localCanData.can_id = 0x123; localCanData.can_dlc = sizeof(hello) / sizeof(hello[0]); sprintf((char *)localCanData.data, hello); cantester.setCanData(localCanData); cantester.start(); // 让程序运行一段时间后停止 std::this_thread::sleep_for(std::chrono::seconds(10)); // 优雅停止线程 cantester.stop(); return 0; }
额外注意点
start方法中的lambda捕获建议使用[this]而非[=],调用成员函数需要访问当前对象,[=]可能引发悬空引用问题:void Canloop::start() { receive_ = std::thread([this] { receiveThread(); }); transmit_ = std::thread([this] { transmitThread(); }); }- 构造函数中
flags != O_NONBLOCK;是无效代码,应改为flags |= O_NONBLOCK;才能正确设置socket为非阻塞模式。
内容的提问来源于stack exchange,提问作者rebelliousconformist
相关产品推荐
相关产品推荐

