译自:http://accu.org/var/uploads/journals/overload104.pdf
这篇文章提供了一种C++模板解决方案来确保在多线程程序中同步访问变量。当我们使用c++编写多线程程序的时候,我们一般会采用如下的模型:
#include <mutex> using namespace std; class Person { public: string GetName() const { unique_lock<mutex> lock(mutex); return mutex; } void SetName(const string& name) { unique_lock<mutex> lock(mutex); this->name = name; }
private: mutex mutex; string name; };
每次当我们想要访问成员变量的时候,我们必须确保先获得了互斥锁。然而,这种模型容易出错。
当我们在使用互斥量时忘记上锁的时候,编译器不会给我们发出警告。
我们需要一种机制来确保同步访问这个成员变量,并且是在锁域内。
这种机制基本是思想是使用两个类:一个作为访问控制器来自动给互斥量加锁及解锁,另一个则作为变量的封装类,
它包含互斥量和这个互斥量所要保护的变量,这个封装器阻止任何对象对这个变量的访问,当然除了这个访问器。这个封装器代码如下:
#include <mutex> using namespace std; template<typename T> class unique_access_guard { //nocopyable private: unique_access_guard(const unique_access_guard&){} unique_access_guard& operator=(const unique_access_guard&){}; public: unique_access_guard():value() {} unique_access_guard(const T& value) :value(value) {} template<typename A1> unique_access_guard(const A1& a1 ) :value(a1) {} template<typename A1,typename A2> unique_access_guard(const A1& a1,const A2& a2) :value(a1,a2) {} //Add Constructors with more arguments, //or use c++0x variadic templates private: friend class unique_access; mutex mutex; T value; };
这个封装器类中能初始化一个值,但是不允许任何对象访问(声明为private),同时使用模板构造函数来避免初始化的时候临时对象的生成(例如T为struct 时)。
访问控制器实现的是一个简单的只能指针,代码实现如下:
template<typename T> class unique_access { //nocopyable private: unique_access(const unique_access&){} unique_access& operator=(const unique_access&){} public: unique_access(const unique_access_guard& guard ) :lock(guard.mutex) ,valueRef(guard.value) {} T& operator*( ) { return valueRef; } T* operator-> () { return &valueRef; } private: unique_lock<mutex> lock; T& valueRef; };
让我们用一个例子来展示这两个类是如何工作的,我们假设已经定义好了const_unique_access类。我们构造一个SafePerson类:
using namespace std; class SafePerson { public: string GetName() const { const_unique_access<string> name(nameGuard); return *name; } void SetName(const string& newName ) { unique_access<string> name(nameGuard); *name = newName; } private: unique_access_guard<string> nameGuard; };
同Person相比,SafePerson中,如果没有给互斥变量上锁,那我们是不能够访问这个变量的,因此,当我们使用一个新的成员函数扩展这个类的时候,我们必须
得给变量上锁。
如果我们的类中拥有很多的成员变量,那么将会发生什么事呢?这依赖于我们的需求。如果成员变量时相互独立的,那么我们可以声明更多的访问保护,并且使用
分离的访问器来访问它们。一个更通用的场景是当我们想要给所有的成员变量上锁的时候,而此时我们去访问它们之间的一个或多个。在这种情况下,我们将使用一个
struct来代替它,这个struct内嵌在class中,如以下代码所示:
using namespace std; class SafeMemberPerson { public: SafeMemberPerson(unsigned int age) :memberGuard(age) {} string GetName() const { const_unique_access<Member> member(memberGuard); return member->name; } void SetName(const string& newName ) { unique_access<Member> member(memberGuard); member->name = newName; } private: struct Member { Member(unsigned int age):age(age) {} string name; int age; }; unique_access_guard<Member> memberGuard; };
另一个通用的场景是私有成员变量的使用,使用正常的mutex和locked方法,我们必须确保在我们调用helper函数的时候成员变量时锁定的。确保这个的通用方法
是在注释中提前做出声明!有了访问保护器我们就能够是这个前提条件显性化了,有两种方案:一是就像在HelperPerson类中所表示的那样,通过引用传递
const_unique_access对象给helper函数,现在我们在没有拥有访问器或锁的情况下是不可能调用helper函数。如以下代码所示:
using namespace std; class HelperPerson { public HelperPerson(unsigned int age):memberGuard(age) {} string GetName( ) const { const_unique_access<Member> member(memberGuard); Invalidate(member); return member; } void SetName(const string& newName ) { unique_access<Member> member<memberGuard>; Invalidate(member); member->name = newName; } private: void Invalidate(const_unique_access<Member>& member) const { if(member->age < 0) throw runtime_error("Age can not negative!\n"); } struct Member { Member(unsigned int age):age(age) { } int age; string name; }; unique_access_guard<Member> memberGuard; };
另一种解决方案是,如MemberHelperPerson所示,那就是使helper成为Member的成员函数。当我们拥有访问器的时候,只要我们解引用Member结构体,我们就能保
证,在Member内的任何成员函数只在我们有锁的情况下调用运行,代码如下所示:
using namespace std; class MemberHelperPerson { public MemberHelperPerson(unsigned int age):memberGuard(age) {} string GetName( ) const { const_unique_access<Member> member(memberGuard); member->Invalidate(); return member; } void SetName(const string& newName ) { unique_access<Member> member<memberGuard>; member->Invalidate(); member->name = newName; } private: struct Member { Member(unsigned int age):age(age) { } void Invalidate( ) const { if(age < 0) throw runtime_error("Age can not negative!\n"); } int age; string name; }; unique_access_guard<Member> memberGuard; };
以上这些都说明单独访问成员变量,这个框架也很容易扩展共享访问。我们需要一个嵌入了std::shared_mutex的shared_access_guard类,和一个shared_access 访问
器来获得成员变量的共享访问权,如果我们想要互斥访问这些共享的成员变量,然后我们可以在shared_access_guard使用unique_access访问器,const用来确保
const_unque_access和shared_access只是读成员变量,而只能用unique_access去写。使用访问器的系统开销是很小的,一个访问控制器的构造和std::unique_lock加上
引用的分配是差不多的。