平时做 C++11 开发时一般不使用thread,是因为thread其实也是基于pthread实现的。并且C++11里面没有提供读写互斥量,而在高并发场景,这些对象是经常需要用到的。那么Modern C++上是否可以实现读写锁呢?
在 C++ 17 之前,要使用 Read/Write Lock 只能自己实现或使用系统提供的 API,但现在情况有所改变,以下就是 C++ 17 读写锁的使用方法:
#include <shared_mutex>
std::shared_mutex g_mutex;
Write Lock 这样写
std::unique_lock<std::shared_mutex> wLock(g_mutex);
Read Lock 这样写
std::shared_lock<std::shared_mutex> rLock(g_mutex);
可以发现 C++ 17 Read/Write lock 的使用就是这么平易近人且直观。
但有一个地方要注意,一般我们都是在成员函数里使用 std::shared_mutex,所以会将它声明为成员变量,另外我们会用 Set 函数来改变成员变量,Get 函数读取成员变量,所以会声明 Get 函数 为 const 成员函数,例子如下:
#include <shared_mutex>
class CStockInfo
{
public:
void SetPrice(double dPrice)
{
std::unique_lock<std::shared_mutex> wLock(m_mutex);
m_dPrice = dPrice;
}
double GetPrice() const
{
std::shared_lock<std::shared_mutex> rLock(m_mutex);
return m_dPrice;
}
private:
double m_dPrice;
std::shared_mutex m_mutex;
};
但此时编译程序会发现编译器在 GetPrice 报错,因为成员函数宣告为 const,此时有的程序员会干脆将 const 拿掉,这是错误的,正确的做法是在声明 std::shared_mutex 变量前加上 mutable,如此就告诉编译器它是逻辑上的常量了。
#include <shared_mutex>
class CStockInfo
{
...
private:
double m_dPrice;
mutable std::shared_mutex m_mutex; // 宣告為 mutable
Mutable 关键词
mutable
从字面意思上来说,是「可变的」之意。
若是要「顾名思义」,那么这个关键词的含义就有些意思了。显然,「可变的」只能用来形容变量,而不可能是「函数」或者「类」本身。然而,既然是「变量」,那么它本来就是可变的,也没有必要使用 mutable
来修饰。那么,mutable
就只能用来形容某种不变的东西了。
C++ 中,不可变的变量,称之为常量,使用 const
来修饰。然而,若是 const mutable
联用,未免让人摸不着头脑——到底是可变还是不可变呢?
事实上,mutable
是用来修饰一个 const
示例的部分可变的数据成员的。如果要说得更清晰一点,就是说 mutable
的出现,将 C++ 中的 const
的概念分成了两种。
- 二进制层面的
const
,也就是「绝对的」常量,在任何情况下都不可修改(除非用const_cast
)。 - 引入
mutable
之后,C++ 可以有逻辑层面的const
,也就是对一个常量实例来说,从外部观察,它是常量而不可修改;但是内部可以有非常量的状态。
当然,所谓的「逻辑
const
」,在 C++ 标准中并没有这一称呼。这只是为了方便理解,而创造出来的名词。
显而易见,mutable
只能用来修饰类的数据成员;而被 mutable
修饰的数据成员,可以在 const
成员函数中修改。
这里举一个例子,展现这类情形。
class HashTable {
public:
//...
std::string lookup(const std::string& key) const
{
if (key == last_key_) {
return last_value_;
}
std::string value{this->lookupInternal(key)};
last_key_ = key;
last_value_ = value;
return value;
}
private:
mutable std::string last_key_
mutable std::string last_value_;
};
这里,我们呈现了一个哈希表的部分实现。显然,对哈希表的查询操作,在逻辑上不应该修改哈希表本身。因此,HashTable::lookup
是一个 const
成员函数。在 HashTable::lookup
中,我们使用了 last_key_
和 last_value_
实现了一个简单的「缓存」逻辑。当传入的 key
与前次查询的 last_key_
一致时,直接返回 last_value_
;否则,则返回实际查询得到的 value
并更新 last_key_
和 last_value_
。
在这里,last_key_
和 last_value_
是 HashTable
的数据成员。按照一般的理解,const
成员函数是不允许修改数据成员的。但是,另一方面,last_key_
和 last_value_
从逻辑上说,修改它们的值,外部是无有感知的;因此也就不会破坏逻辑上的 const
。为了解决这一矛盾,我们用 mutable
来修饰 last_key_
和 last_value_
,以便在 lookup
函数中更新缓存的键值。
其实简单来讲,mutable意思是“这个成员变量不算对象内部状态”。
比如,你搞了个变量,用来统计某个对象的访问次数(比如供debug用)。它变成什么显然并不影响对象功用,但编译器并不知道:它仍然会阻止一个声明为const的函数修改这个变量。
把这个计数变量声明为mutable,编译器就明白了:这个变量不算对象内部状态,修改它并不影响const语义,所以就不需要禁止const函数修改它了。