前言

在C++多线程程序的开发过程中,资源的互斥访问是第一个要考虑的问题,常用的方法就是使用互斥锁对共享数据进行保护,而使用锁最大的风险就是有可能产生死锁,导致程序异常退出。因为这个风险的存在,所以我每次使用互斥锁的时候都小心翼翼,看互斥锁lock之后是否在合适的地方进行了unlock,在多个互斥锁同时使用的时候反复检查加锁的顺序是否会导致死锁……但是,百密一疏,自己还是亲手写出了一把死锁,导致付出了很多时间和精力去Debug。

死锁的诞生

在测试股票自动化交易软件的时候,软件运行一定时间之后会突然失去响应并且崩溃,而且问题无法复现,后来经过打印log去打印出每次运行后各个变量的值,期望还原“事故现场”,经过反复测试,发现当资金量较小时程序会崩溃,联想到最近修改的资金量相关代码,最终将问题锁定到策略线程中一段代码:

1
2
3
4
5
6
7
8
//判断资金量是否足够完成本次交易
g_MutexCash.lock();
if (quantity * 100 * g_mapStockList[stockcode].get_match_price() > g_iCash) //购买所需资金大于可用资金,忽略该信号
return false; //导致死锁!!!!
std::cout << "当前资金:" << g_iCash / 10000 << "元" << std::endl;
g_iCash = g_iCash - 100 * quantity * g_mapStockList[stockcode].get_match_price(); //买入股票,资金量减少
std::cout << "本次交易花费:" << quantity * g_mapStockList[stockcode].get_match_price() / 100 << "元" << std::endl;
g_MutexCash.unlock();

我们分析一下这段代码,由于全局变量g_iCash是所有线程共享的,所以需要使用互斥锁g_MutexCash进行保护,在我们读写g_iCash变量之前,先调用g_MutexCash.lock()加锁,然后对g_iCash读写完毕之后,调用g_MutexCash.unlock()进行解锁。看似没有问题,实际这段代码是会产生死锁的。代码的3,4两行我们判断当前资金是否大于本次交易耗费资金,如果当前资金不足,则直接放弃本次交易,调用return返回,当我们在这里return之后,return后面的语句都不会再继续执行,所以g_MutexCash.unlock()语句不会被调用,所以导致互斥锁永远不会被释放,最终导致死锁。

出现问题原因

相信稍微有点多线程编程经验的同学都会很轻易的发现上述代码可能会产生死锁,而且在我们每次使用互斥锁都小心使用,这段代码为什么会犯这种低级错误?

其实上面代码初始版本是不包含第3、4两行的,当不存在3、4两行时,我们这样写是没有问题的,但是由于后面软件需求更改,我们需要加入第3、4行的判断资金量逻辑,所以在原来代码基础上直接添加上了第3、4行,添加的时候却忘记了这段代码处于临界区中,没有仔细考虑添加代码之后会不会产生死锁,进而导致了出现了上述代码中的低级错误。

总结

通过这个问题我们可以总结出如下经验:

  1. 每次进行代码的修改的时候,一定要对整个程序的结构了解透彻,要对这段代码的修改对整个程序产生的影响有所认知,避免出现修复一个Bug的同时引入了N个新Bug的情况。
  2. 对于互斥锁的使用:避免使用“裸”互斥锁,而要使用lock_guard或者unique_lock持有互斥锁,这样互斥锁的作用域便于unique_lock的生命周期相同,比如上面代码我们就可以使用unique_lock持有mutex,当第4行return的时候,unique_lock由于出了作用域被析构,析构的同时释放了互斥锁,所以便不会有死锁产生。
  3. 对于一些简单的变量,可以使用原子变量,而不必用互斥锁进行保护,无锁编程在提高了一定效率的同时还能避免死锁的产生。相关的问题我们在文章C++性能榨汁机之无锁编程中进行了详细的描述。