这几天在写自己项目的日志组件,但是遇见了一个很严重的问题。就是关于多线程中对象生命周期的问题。
问题是这样的:
首先说明我的日志组件一共有两个类:block_queue、log,block queue是用来支持日志异步写日志的。
在我打算写一个自己用的日志组件的时候,我想用到我之前已经封装好的thread类,以及用mutex和condition类来实现线程同步。在我把代码大概写好了之后。日志库一运行,就出现:assertion ~mutex() failed, 或者assertion ~condition failed,出现assertion是因为我有习惯对一些易出错的函数进行assert返回值判断。这个时候我并不慌乱
因为我凭借assert已经知道是在它们俩的析构函数中出了问题。然后我开始用GDB调试,当我一直往下走的时候,走到最后会出现pthread_mutex_destroy.c:not such file or diectoy,或者pthread_cond_destroy.c no such failed。我很奇怪,我的mutex和conditon是利用RAII机制封装的,按道理来说出现死锁或者其他问题的概率是很小的,而且这种
no such file or directory.c的错误甚至让我联想到难道是我缺少这个文件。但是这不可能,以前一直在用的。然后我用GDB,直接run出现signal终止后,查看函数调用栈,出现最后调用就是no such file的调用。这个问题很奇怪,我尝试把assert断言换成perror,想要打印错误号,结果出现的输出错误竟然是.success。
一:条件变量cond析构错误
为了摸清到底拿出了问题,于是我用#ifdef _DEBUG_写了好多打印,用来帮助我检错,结果发现有的时候程序一直不能没有跳出阻塞队列对队空时候的循环。
阻塞队列是生产者消费者模型,取任务的功能在take()函数中实现。当队列中元素为空时,条件变量会一直处于wait状态。当有队中放入元素时,会唤醒该条件变量,然后消费者take取任务,开始工作。为了防止惊群效应,在这里用了while循环。
最初的版本是:
while(queue_.empty()){
not_empty_.wait();
}
但是我当时还处于no such file之中,完全不知道哪里出了问题。然后一遍又一遍查看代码逻辑,没有发现问题。并且多线程的程序运行逻辑其实是很难用肉眼看清的。为了解决这个问题,我开始在网上搜GDB 调试多线程程序的方法,因为以前代码量比较少我没用GDB调试过,然后查找了一些资料了打印线程信息,以及切换线程的命令,然后开始用GDB进行多线程调试,到信号终止时,打印函数调用栈,切换线程,再打印函数调用栈,发现一个最后处于wait状态,一个处于no such file。
这个no such file不明不白,我想到去查看condition的man手册,想要在官方文档中找找问题。
注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量。
然后我想到了我是在这个cond处于wait状态时,企图destroy它,出现了问题,我发现我没有在析构函数中唤醒该条件变量,然后我在析构函数中果断加了notify,然后重新测试,结果又出现assertion。
为这个问题纠结了很长时间。
后来发现,如果我调用该日志模块,但是没有写入任何日志的时候,阻塞队列一直都是空的,那么它就会一直处于while循环之中。为了证实我这个猜想,我在while循环中加了打印,发现它果然调用了两次。我就想到一个办法,既然析构的时候要让他跳出循环,我就在成员变量中加了一个标记,默认为false,析构时为true,这样就可以在析构的时候也能让它退出等待。
二:mutex析构错误
-->mutex_lock_guard是我用RAII机制封装的一个类,它的内部再构造函数会对构造函数参数mutex进行加锁,析构函数会对其进行解锁,在我的block queue中,这个mutex为了共享,它是作为了block queue的成员。
在take函数中,为了保证从队列中取任务的线程安全,所以加锁保护。
结果还是出现assertion或段错误,只不过换成了~mutex返回值错误,为这个问题我纠结了一天,因为我认为是在block queue中出现的问题,一直在想block queue中的逻辑是否出现了问题,既然都退出wait状态了,程序不会阻塞在这里,为什么~mutex会出错呢?
不管是用GDB直接查看还是用GDB查看段错误core文件,问题仍然是没有头绪。
又纠结了两天,在网上查找资料,没有头绪。我明明析构的时候已经唤醒了,为什么还是错误呢。
后来我想到了一种可能的情况,就是我的block queue可能出现了“身先死,成员变量未死“,因为我的block queue的成员mutex是利用RAII机制封装好的现成的类,我的代码是这样的:
template <typename T>
T block_queue<T>::take()
{
/*
modify: mutex_lock_guard lock(mutex_);
date : 2016.11.17
*/
mutex_lock_guard lock(mutex_);
while(queue_.empty() && !is_destroy_){
not_empty_.wait();
#ifdef _DEBUG_
std::cout<<"wait"<<std::endl;
#endif
}
if(!queue_.empty()){ //!!!!!!!!!add
assert(!queue_.empty());
T front(queue_.front()); //using queue.front to create a T type object
queue_.pop_front();
return front;
}
return T(0);
}
我猜想,在我程序退出wait循环时,我的mutex对象可能还没有死,block queue就死了。为了证实我的猜想,我在block queue的析构函数中加上了sleep(1)函数来进行延时测试,我发现如果让block queue函数延时几秒析构,果然程序没有错误发生。
我又检查了程序,析构函数已经控制条件变量cond跳出while循环,该take()函数完结,那么mutex类自然就会析构,怎么会出现”身先死,成员未死呢“?
来回看了好多遍程序,我突然想明白了,take()函数中条件变量类退出循环,该函数会朝下执行,但是由于是多线程的析构函数也会同时向下执行,那么问题出来了:
如果该成员函数的执行的速度没有析构函数执行的速度快,该成员函数还未执行完毕时,析构函数已经执行完毕。然后该成员函数中局部的mutex类触发RAII机制,试图去析构
起始是调用pthread_mutex_destroy(mutex_),那么这时就会出现错误,由于参数mutex_也是block queue的成员,此时block queue已经析构掉了,那么它的成员变量mutex就已经不存在了,这是调用一定会出错的!
然后我明白了这一定是线程执行的顺序的问题,就查看了一下我的log类,我的log类中有一个block_queue的成员,log类在进行异步写日志时,会创建线程调用block queue中的成员函数take()函数读取从队列中读取日志信息,但是我没有在log类中加上thread join,问题就出在这里!
如果我没有加thread join的话,加入log类析构掉了,没有等待它创建出的线程,那么log类的成员block queue会先于该线程结束前析构掉!
所以我在log类中加入了thread.join()函数之后,问题迎刃而解。
关于这次这个问题的总结:
1.理解了C++多线程下的生命周期管理。
2.深刻深刻深刻的体会到了多线程程序执行顺序的不确定性!
PS:这个问题折磨了我四天!好头大!