Item 16: 让const成员函数做到线程安全
本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!
博客已经迁移到这里啦
如果我们在数学领域里工作,我们可能会发现用一个类来表示多项式会很方便。在这个类中,如果有一个函数能计算多选式的根(也就是,多项式等于0时,各个未知量的值)将变得很方便。这个函数不会改变多项式,所以很自然就想到把它声明为const:
class Polynomial{
public:
using RootsType = //一个存放多项式的根的数据结构
std::vector<double>; //(using的信息请看Item 9)
...
RootsType roots() const;
...
};
计算多项式的根代价可能很高,所以如果不必计算的话,我们就不想计算。如果我们必须要计算,那么我们肯定不想多次计算。因此,当我们必须要计算的时候,我们将计算后的多项式的根缓存起来,并且让roots函数返回缓存的根。这里给出最基本的方法:
class Polynomial{
public:
using RootsType = std::vector<double>;
RottsType roots() const
{
if(!rootsAreValid){ //如果缓存不可用
... //计算根,把它们存在rootVals中
rootsAreValid = true;
}
return rootVals;
}
private:
mutable bool rootsAreValid{ false }; //初始化的信息看Item 7
mutable RootsType rootVals{};
};
概念上来说,roots的操作不会改变Polynomial对象,但是,对于它的缓存行为来说,它可能需要修改rootVals和rootsAreValid。这就是mutable很经典的使用情景,这也就是为什么这些成员变量的声明带有mutable。
现在想象一下有两个线程同时调用同一个Polynomial对象的roots:
Polynomuial p;
...
/*-------- 线程1 -------- */ /*-------- 线程2 -------- */
auto rootsOfP = p.roots(); auto valsGivingZero = p.roots();
客户代码是完全合理的,roots是const成员函数,这就意味着,它表示一个读操作。在多线程中非同步地执行一个读操作是安全的。至少客户是这么假设的。但是在这种情况下,却不是这样,因为在roots中,这两个线程中的一个或两个都可能尝试去修改成员变量rootsAreValid和rootVals。这意味着这段代码在没有同步的情况下,两个不同的线程读写同一段内存,这其实就是data race的定义。所以这段代码会有未定义的行为。
现在的问题是roots被声明为const,但是它却不是线程安全的。这样的const声明在C++11和C++98中都是正确的(取多项式的根不会改变多项式的值),所以我们需要更正的地方是线程安全的缺失。
解决这个问题最简单的方式就是最常用的办法:使用一个mutex:
class Polynomial{
public:
using RootsType = std::vector<double>;
RottsType roots() const
{
std::lock_guard<std::mutex> g(m); //锁上互斥锁
if(!rootsAreValid){ //如果缓存不可用
...
rootsAreValid = true;
}
return rootVals;
} //解开互斥锁
private:
mutable std::mutex m;
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};
std::mutex m被声明为mutable,因为对它加锁和解锁调用的都不是const成员函数,在roots(一个const成员函数)中,如果不这么声明,m将被视为const对象。
值得注意的是,因为std::mutex是一个move-only类型(也就是,这个类型的对象只能move不能copy),所以把m添加到Polynomial中,会让Polynomial失去copy的能力,但是它还是能被move的。
在一些情况下,一个mutex是负担过重的。举个例子,如果你想做的事情只是计算一个成员函数被调用了多少次,一个std::atomic计数器(也就是,其它的线程保证看着它的(counter的)操作不中断地做完,看Item 40)常常是达到这个目的的更廉价的方式。(事实上是不是更廉价,依赖于你跑代码的硬件和标准库中mutex的实现)这里给出怎么使用std::atomic来计算调用次数的例子:
class Point {
public:
...
double distanceFromOrigin() const noexcept //noexcept的信息请看Item 14
{
++callCount; //原子操作的自增
return std::sqrt((x * x) + (y * y));
}
private:
mutable std::atomic<unsigned> callCount{ 0 };
};
和std::mutex相似,std::atomic也是move-only类型,所以由于callCount的存在,Point也是move-only的。
因为比起mutex的加锁和解锁,对std::atomic变量的操作常常更廉价,所以你可能会过度倾向于std::atomic。举个例子,在一个类中,缓存一个“计算昂贵”的int,你可能会尝试使用一对std::atomic变量来代替一个mutex:
class Widget {
public:
...
int magicValue() const
{
if (cacheValid) return cachedValue;
else{
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2; //恩,第一部分
cacheValid = true; //恩,第二部分
return cachedValue;
}
}
private:
mutable std::atomic<bool> cacheValid { false };
mutable std::atomic<int> cachedValue;
};
这能工作,但是有时候它会工作得很辛苦,考虑一下:
- 一个线程调用Widget::magicValue,看到cacheValid是false的,执行了两个昂贵的计算,并且把它们的和赋给cachedValue。
- 在这个时间点,第二个线程调用Widget::magicValue,也看到cacheValid是false的,因此同样进行了昂贵的计算(这个计算第一个线程已经完成了)。(这个“第二个线程”事实上可能是一系列线程,也就会不断地进行这昂贵的计算)
这样的行为和我们使用缓存的目的是相违背的。换一下cachedValue和CacheValid赋值的顺序可以消除这个问题(不断进行重复计算),但是错的更加离谱了:
class Widget {
public:
...
int magicValue() const
{
if (cacheValid) return cachedValue;
else{
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValid = true; //恩,第一部分
return cachedValue = val1 + val2; //恩,第二部分
}
}
...
};
想象一下cacheValid是false的情况:
- 一个线程调用Widget::magicValue,并且刚执行完:把cacheValid设置为true。
- 同时,第二个线程调用Widget::magicValue,然后检查cacheValid,发现它是true,然后,即使第一个线程还没有把计算结果缓存下来,它还是直接返回cachedValue。因此,返回的值是不正确的。
让我们吸取教训。对于单一的变量或者内存单元,它们需要同步时,使用std::atomic就足够了,但是一旦你需要处理两个或更多的变量或内存单元,并把它们视为一个整体,那么你就应该使用mutex。对于Widget::magicValue,看起来应该是这样的:
class Widget {
public:
...
int magicValue() const
{
std::lock_guard<std::mutex> guard(m); //锁住m
if (cacheValid) return cachedValue;
else{
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
} //解锁m
...
private:
mutable std::mutex m;
mutable int cachedValue; //不再是atomic了
mutable bool cacheValid { false };
};
现在,这个Item是基于“多线程可能同时执行一个对象的const成员函数”的假设。如果你要写一个const成员函数,并且你能保证这里没有多于一个的线程会执行这个对象的cosnt成员函数,那么函数的线程安全就不重要了。举个例子,如果一个类的成员函数只是设计给单线程使用的,那么这个成员函数是不是线程安全就不重要了。在这种情况下,你能避免mutex和std::atomic造成的负担。以及免受“包含它们的容器将变成move-only”的影响。然而,这样的自由线程(threading-free)变得越来越不常见了,它们还将变得更加稀有。以后,const成员函数的多线程执行一定会成为主题,这就是为什么你需要确保你的const成员函数是线程安全的。
你要记住的事
- 让const成员函数做到线程安全,除非你确保它们永远不会用在多线程的环境下。
- 比起mutex,使用std::atomic变量能提供更好的性能,但是它只适合处理单一的变量或内存单元
Item 16: 让const成员函数做到线程安全的更多相关文章
- 类1(this指针/const成员函数/类作用域/外部成员函数/返回this对象的函数)
假设我们要设计一个包含以下操作的 Sales_data 类: 1.一个 isbn 成员函数,用于返回对象的 book_no 成员变量 2.一个 combine 成员函数,用于将一个 Sales_dat ...
- const成员函数
尽管函数名和参数列表都相同,void foo( ) const成员函数是可以与void foo( )并存的,可以形成重载! 我们假设调用语句为obj.foo(),如果obj为non-const对象,则 ...
- c++ 学习之const专题之const成员函数
一些成员函数改变对象,一些成员函数不改变对象. 例如: int Point::GetY() { return yVal; } 这个函数被调用时,不改变Point对象,而下面的函数改变Point对象: ...
- 关于 const 成员函数
成员函数如果是const意味着什么? 有两个流行概念:物理常量性和逻辑常量性. C++对常量性的定义采用的是物理常量性概念,即const 成员函数不可以更改对象内任何non-static成员变量.例如 ...
- C++ Const成员函数
一些成员函数改变对象,一些成员函数不改变对象. 例如: int Point::GetY() { return yVal; } 这个函数被调用时,不改变Point对象,而下面的函数改变Point对象 ...
- C++类的const成员函数、默认的构造函数、复制形参调用函数(转)
C++类的const成员函数 double Sales_item::avg_price() const { } const关键字表明这是一个const成员函数,它不可以修改Sales_item类的成员 ...
- c++中的const参数,const变量,const指针,const对象,以及const成员函数
const 是constant 的缩写,“恒定不变”的意思.被const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性.所以很多C++程序设计书籍建议:“Use const whe ...
- 拷贝构造函数和const成员函数
实验原因 说明如何使用const描述保护类数据不会意外修改. 编译环境 vc6sp6 + win7x64 工程下载 copyConstruction_constMemberFunction.zip ...
- 12.C++-构造函数与析构函数调用顺序,const成员函数,const对象
单个对象创建时,构造函数的调用顺序 1.首先判断该对象的类是否拥有父类,若有则先调用父类的构造函数 2.判断该对象的成员是否是其它类的成员,若是则调用成员变量的构造函数(调用顺序和声明顺序相同) 3. ...
随机推荐
- 使用 new XMLHttpRequest() 制作下载文件进度条
mui 进度控件使用方法: 检查当前容器(container控件)自身是否包含.mui-progressbar类: 当前容器包含.mui-progressbar类,则以当前容器为目标控件,直接显示进度 ...
- mysql服务自启【Linux】
1.复制启动脚本到资源目录 2.增加mysqld服务控制脚本执行权限 3.mysql服务添加到系统服务 4.检测mysqld服务是否生效 表明服务已经启动,以后可以使用service命令启动mysql ...
- EOS智能合约开发(二):EOS创建和管理钱包
上节介绍了EOS智能合约开发之EOS环境搭建及启动节点 那么,节点启动后我们要做的第一件事儿是什么呢?就是我们首先要有账号,但是有账号的前提是什么呢?倒不是先创建账号,而是先要有自己的一组私钥,有了私 ...
- Django基础笔记
1.准备工作 .虚拟环境设置 python3 pip install virtualenv python -m venv env(虚拟环境文件名) env\Scripts\activate pip i ...
- 在Linux系统上利用Tomcat搭建测试环境
第一歩:查看Linux系统的IP地址. 输入命令:ifconfig 第二歩:WinSCP工具 1.下载WinSCP工具,便于文件直接从windows系统直接拖动到Linux系统中,图形化创建文件夹等. ...
- GitHub-版本管理
参考博文:廖雪峰Git教程 1. 管理修改 现在,假定你已经完全掌握了暂存区的概念.下面,我们要讨论的就是,为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件. 你会问, ...
- BIZHUB184打印机提示维修召唤(m2)修复
其他不用管,按照操作直接干:菜单键--常用设置--左键---左键---常用设置--左键---右键 咦 神奇的进入了service mode 服务模式 选择CLEAR DATA 项---- ...
- LeetCode算法题-Range Sum Query Immutable(Java实现)
这是悦乐书的第204次更新,第214篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第70题(顺位题号是303).给定整数数组nums,找到索引i和j(i≤j)之间的元素之 ...
- Tomcat安装、配置和部署笔记
首先从Apache的官方网站(http://tomcat.apache.org/)下载Tomcat.有安装版和解压版两种,我个人喜欢用解压版. Tomcat安装(绿色版安装) 1.将下载的Tomcat ...
- Nginx 的 TCP 负载均衡介绍
Nginx除了以前常用的HTTP负载均衡外,Nginx增加基于TCP协议实现的负载均衡方法. HTTP负载均衡,也就是我们通常所有“七层负载均衡”,工作在第七层“应用层”.而TCP负载均衡,就是我们通 ...