Journal Articles

Overload Journal #104 - August 2011 + Programming Topics
Browse in : All > Journals > Overload > o104 (6)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Thread-Safe Access Guards

Author: Martin Moene

Date: 03 August 2011 19:42:02 +01:00 or Wed, 03 August 2011 19:42:02 +01:00

Summary: Ensuring safe access to shared data can be cumbersome and error-prone. Bjørn Reese presents a technique to help.

Body: 

This article describes a C++ template solution for ensuring synchronized access to variables in multi-threaded applications.

When doing multi-threaded programming in C++, we frequently come across this pattern seen in Listing 1.

class Person
{
public:
  std::string GetName() const
  {
    std::unique_lock<std::mutex> lock(mutex);
    return name;
  }
  void SetName(const std::string& newName)
  {
    std::unique_lock<std::mutex> lock(mutex);
    name = newName;
  }
private:
  std::mutex mutex;
  std::string name;
};
			
Listing 1

Every time we want to access the member variable, we must make sure that we lock the mutex. Unfortunately this pattern is error-prone. The compiler cannot alert us when we have forgotten to lock the variable before we use it.

We really would like kind of some mechanism that made sure that access to the member variables always is synchronized, and where we can control the locking scope.

The basic idea is to have two classes. An accessor that automatically locks and unlocks the mutex, and a variable wrapper that contains the mutex and the variable we wish to protect with the mutex. The wrapper prevents any access to the variable except through the accessor.

We will start with the wrapper (Listing 2).

template<typename T>
class unique_access_guard : noncopyable
{
public:
  unique_access_guard()
    : value() {}
  unique_access_guard(const T& value) 
    : value(value) {}

  template<typename A1>
  unique_access_guard(const A1& arg1)
    : value(arg1) {}
  template<typename A1, typename A2>
  unique_access_guard(const A1& arg1,
                      const A2& arg2)
    : value(arg1, arg2) {}
  // Add constructors with more arguments,
  // or use C++0x variadic templates
private:
  friend class unique_access<T>;
  std::mutex mutex;
  T value;
};
			
Listing 2

Here we have a class that can be initialized with a value, but does not allow anybody to access its value. The templated constructors are used to avoid temporary objects during intialization if T is a struct.

The accessor is a very simple smart pointer and will then look like Listing 3.

template<typename T>
class unique_access : noncopyable
{
public:
  unique_access(unique_access_guard& guard)
    : lock(guard.mutex),
      valueRef(guard.value)
  {}

  T& operator* () { return valueRef; }
  T* operator-> () { return &valueRef; }

private:
  std::unique_lock<std::mutex> lock;
  T& valueRef;
};
			
Listing 3

Let us continue with an example of how this works. We will assume that we also have defined a const_unique_access class. This can be implemented using the slicing technique. (Listing 4.)

class SafePerson
{
public:
  std::string GetName() const
  {
    const_unique_access<std::string>
       name(nameGuard);
    return *name;
  }

  void SetName(const std::string& newName)
  {
    unique_access<std::string> name(nameGuard);
    *name = newName;
  }

private:
  unique_access_guard<std::string> nameGuard;
};
			
Listing 4

Compared to the Person class, we cannot access the name member variable without locking it, so we cannot forget the lock when we are extending the class with a new member function.

What happens if our class has many member variables? That depends on our needs. If the member variables are independent from each other, then we can declare more access guards and use separate accessors for them. A more common scenario is when we want to lock all member variables when we access one or more of them. In that case we can place them in a struct. This struct can be nested in the class, as shown in Listing 5.

class SafeMemberPerson
{
public:
  SafeMemberPerson(unsigned int age) 
     : memberGuard(age) {}

  std::string GetName() const
  {
    const_unique_access<Member> 
       member(memberGuard);
    return member->name;
  }

  void SetName(const std::string& newName)
  {
    unique_access<Member> member(memberGuard);
    member->name = newName;
  }

private:
  struct Member
  {
    Member(unsigned int age) : age(age) {}

    std::string name;
    unsigned int age;
  };
  unique_access_guard<Member> memberGuard;
};
			
Listing 5

Another common scenario is the use of private helper member functions. With the normal mutex and lock method we must make sure that the member variables are locked before we call the helper function. The usual way to ensure this is to document the precondition in a comment.

With the access guards we can make this precondition explicit. There are two solutions. The first, shown in the HelperPerson class, is to pass the const_unique_access object by reference to the helper function. Now it is not possible to call the helper function unless we have an accessor and hence a lock. (See Listing 6.)

class HelperPerson
{
public:
  HelperPerson(unsigned int age)
    : memberGuard(age)
    {}

  std::string GetName() const
  {
    const_unique_access<Member>
       member(memberGuard);
    Invariant(member);
    return member->name;
  }

  void SetName(const std::string& newName)
  {

    unique_access<Member> member(memberGuard);
    Invariant(member);
    member->name = newName;
  }

private:
  void Invariant(
     const_unique_access<Member>& member) const
  {
    if (member->age < 0)
       throw std::runtime_error(
       "Age cannot be negative");
  }

  struct Member
  {
    Member(unsigned int age) : age(age) {}

    std::string name;
    unsigned int age;
  };
  unique_access_guard<Member> memberGuard;
};
			
Listing 6

The other solution, which is shown in the MemberHelperPerson class, is to let the helper be a member function of the Member struct. As we only can deference the Member struct when we have an accessor, we are guaranteed that any member function in the Member struct only runs when we have a lock. (Listing 7)

class MemberHelperPerson
{
public:
  MemberHelperPerson(unsigned int age)
    : memberGuard(age) {}

  std::string GetName() const
  {
    const_unique_access<std::string>
       member(memberGuard);
    member->Invariant();
    return member->name;
  }

  void SetName(const std::string& newName)
  {
    unique_access<std::string>
       member(memberGuard);
    member->Invariant();
    member->name = newName;
  }

private:
  struct Member
  {
    Member(unsigned int age) : age(age) {}

    void Invariant() const
    {
      // We always have unique access to the member
      // variables here
      if (age < 0)
        throw std::runtime_error(
           "Age cannot be negative");
    }

    std::string name;
    unsigned int age;
  };
  unique_access_guard<Member> memberGuard;
};
			
Listing 7

All of the above has been illustrated with unique access to member variables. The framework can be easily extended to encompass shared access as well. We need a shared_access_guard that embeds a std::shared_mutex, and a shared_access accessor to gain shared access to the member variables. If we want unique access to these sharable member variables, then we can use the unique_access accessor on the shared_access_guard.

Const correctness is used to ensure that const_unique_access and shared_access can only read member variables, and only unique_access can write them.

The overhead of using accessor guards is minimal. The construction of an accessor is the same as a std::unique_lock plus the assignment of a reference. The subsequent use of an accessor is the extra level of indirection from operator-> and operator*. Some of this will be removed by the optimizer though.

The advantages of using access guards is:

The access guards have been designed to be simple. This means that there are several of the less common use cases for locking that they do not support.

Their limitations are:

The technique described here has some commonality with the SynchronizedValue::Updater [Dobbs].

The main differences are that the guards do not allow access to the variables, as SynchronizedValue does, and the accessors can used with different guards, thus separating the type of access (exclusive or shared) which is a property of the accessor, not the guard.

Reference

[Dobbs] http://www.drdobbs.com/cpp/225200269

Notes: 

More fields may be available via dynamicdata ..