问题描述
在项目某次开发中,测试过程中出现了coredump问题。经过asan工具检测,报了heap-use-after-free
内存错误,最终定位到竟是无意中添加了一个引用&
导致的!
开发时因为看到相关类访问类成员的接口函数未返回引用,而是返回了一个拷贝,因此想着要将返回值改为引用,避免多余的拷贝。例如在以下代码中,我们将GetArr
函数的返回值改为引用:
class Foo {
private:
std::vector<int> arr;
public:
std::vector<int>& GetArr() { // add & for return type
return arr;
}
// other functions
};
看起来这并没什么问题,但是在多线程环境下,加一个&
可能就会导致程序崩溃。
问题分析
比如以下代码展示了一个简单的例子,表面看起来GetArr
是有锁保护的,但实际上锁保护被打破了:
class Foo {
private:
std::vector<int> arr;
std::mutex mut;
public:
std::vector<int>& GetArr() {
std::lock_gurad<std::mutex> lk(mut);
return arr;
}
// other functions
};
为什么呢?GetArr
返回一个 vector 的引用,然后立即释放锁。因此,返回的引用可以在没有任何保护的情况下被修改。当多个线程对同一个Foo实例的arr进行读写时,就会导致数据竞争,从而导致程序崩溃的风险。
如何避免?
- 对于类对象的多线程共享数据,接口应返回拷贝而不是引用。
std::vector<int> GetArr() { // return a copy
return arr;
}
总结
再翻翻《C++并发实战》,才发现书中早已经总结了这个陷阱: 通常来说,类的所有成员函数在访问任何数据成员之前,假如都先对互斥加锁,并在完成访问后解锁,共享数据就可很好地受到全方位保护。可惜事与愿违:如果类的成员函数返回指向共享数据的指针或引用,那么这些数据就会在锁外被访问,从而破坏了互斥的效果。
因此,我们可以总结一个简单的规则:不得向锁所在的作用域之外传递指针和引用,指向受保护的共享数据,无论是通过函数返回值将它们保存到对外可见的内存,还是将它们作为参数传递给使用者提供的函数。
参考
- C++ Concurrency in Action, Anthony Williams
你好,我是七昂,致力于分享C++、计算机底层、机器学习等系列知识。希望我们能一起探索程序员修炼之道。如果我的创作内容对您有帮助,请点赞关注。如果有问题,欢迎随时与我交流。感谢你的阅读。
知乎、公众号:七昂的技术之旅