一个BUG

来源:互联网 发布:nginx rtmp 延时配置 编辑:程序博客网 时间:2024/04/27 16:49

解决了一个很隐蔽的BUG,就在刚才。

写了一个类,其对象实例需在多线程中使用,因此打算把互斥机制封装在类的内部。

于是在类里定义了一个boost::mutex类型的变量,取名叫“mutex_”

在成员函数中使用的时候,用这个mutex_对象初始化一个局部锁:

mutex::scoped_lock lock(mutex_);

然后写了个单元测试,创建三个线程,每个线程调用对象的某成员函数1000次,通过观察对象的最终的状态来确定其行为跟我想要的是否一致。

单元测试顺利通过。

于是继续做其它事情,写测试用例,编码,编译,测试……如此重复。突然,在测一个新的东西时,却发现前面的这个测试失败了一次。再次运行这个测试,却又成功了,又多次运行,还是成功。

隐约觉得这里边可能隐藏着大问题,十有八九是我对互斥体的使用上还是没考虑周到,结果导致了竞争条件。

可问题是,唯一的一次失败总是不可重现。咋办?这是首要的问题,一定要想个办法让它容易重现,最好是“一定失败”,这样才能使问题的原因更容易查找。

曾考虑让每个线程在每调用完一次被测函数后,执行一次this_thread::yield();让出CPU所有权。随即又打消了这个念头,因为直观看来,这只会使竞争条件更难以暴露。于是,采用更简单的方式,把调用次数从1000次提到一万次,还是没有重现,10万次,还是不行,100万,也不行。

咋这么别扭呢,再试一下1000万吧_这次行了,测试失败了。又将测试运行了10多次,失败率100%。

从测试程序的输出来看,每次被测对象的状态都不一样,有各式各样的取值,这种不确定性更说明竞争条件存在的可能性比较大。

是不是CppUnit并行执行同一个test suite中的各个test case呢?到网上找了一下,没找到有意义的资料。回头想想应该不会,要是那样的话,同一个test suite中,如果多个test case共享使用测试类中定义的成员变量,岂不是也要使用mutex来写了?在测试类中定义成员变量并在测试函数中访问的情况很多,但没见过谁在动不动就在测试类中定义互斥体,所以应该不会。

怎么办?看代码吧。简单的几行,自认为即使在抛出异常的情况下也是线程安全的。究竟是怎么回事呢?没办法,再仔细看。结果一下子看出来了。。。

原来,是一点小小的疏忽,把

mutex::scoped_lock lock(mutex_);

误写成了:

mutex::scoped_lock lock(mutex);

马上改回来。好了,现在成功率100%。

又把次数改成1亿次,仍然OK——就是测试时间不可忍受,于是改回了1000万,后来又试着改成了500万,发现在写错的情况下,重现率仍然100%,就那样放着了。

转念一想还是不对,误将变量名写成类名,编译器为啥不报错呢?难道又是模板实例化及模板参数推导过程中的那些微妙的规则在作怪?仔细一想,自己也笑了,这一个“疏忽”,含义可大不一样了,mutex::scoped_lock lock(mutex_);是定义一个局部锁对象,用成员变量mutex_来初始化它;而mutex::scoped_lock lock(mutex);只是在局部声明了一个函数(返回类型为mutex::scoped_lock,参数类型为mutex)——声明一个函数不会产生任何目标代码,也就是说,实际上没有任何互斥发生。

为什么原来次数少的时候那么难以重现呢。我想原因无非是次数太少,实际上又没有互斥,调用一个小函数1000次,以现在的CPU速度,一个线程在一次时间片中估计就搞定了,于是三个线程实际上还是串行的。后来次数增加了,线程或许偶尔被切换一次,也碰巧竞争条件没有发作。

bug解决了,能说明啥呢?1000万?这这个数字能算啥呢?硬件会越来越快,所以它根本没有任何参考价值。唉,要不怎么说解bug需要经验、方法,再加一点运气呢。

不过,我还是庆幸自己写了那个单元测试,更感谢上帝让它失败了那一次,也庆幸自己能够抓住不放,最终把原因揪了出来。