Enforcing Correct Mutex Usage with Synchronized Values

原文来自Dr.Dobb's Enforcing Correct Mutex Usage with Synchronized Values

Ensuring that the correct mutex is locked when accessing data 

Anthony Williams is author of the bookC++ Concurrency in Action and of the just::thread C++0x thread library. He can be contacted at anthony.ajw@gmail.com.


In Associate Mutexes with Data to Prevent Races, Herb Sutter examined the importance of ensuring that the correct mutex is locked when accessing data, then presented one technique for achieving this. In this article, I present an alternative technique that I have used to achieve the same aims, in the form of the SynchronizedValue class template.

The Problem with Mutexes

The key problem with protecting shared data with a mutex is that there is no easy way to associate the mutex with the data. It is thus relatively easy to accidentally write code that fails to lock the right mutex --- or even locks the wrong mutex --- and the compiler will not help you.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::mutex m1;
int value1;
std::mutex m2;
int value2;
 
int readValue1()
{
     std::lock_guard<std::mutex> lk(m1);
     return value1;
}
int readValue2()
{
     std::lock_guard<std::mutex> lk(m1); // oops: wrong mutex
     return value2;
}

Moreover, managing the mutex lock also clutters the source code, making it harder to see what is really going on.

The use of SynchronizedValue<T> solves both these problems --- the mutex is intimately tied to the value, so you cannot access it without a lock, and yet access semantics are still straightforward. For simple accesses, SynchronizedValue<T> behaves like a pointer-to-T; for example:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
SynchronizedValue<std::string> value3;
std::string readValue3()
{
     return *value3;
}
void setValue3(std::string const& newVal)
{
     *value3=newVal;
}
void appendToValue3(std::string const& extra)
{
     value3->append(extra);
}

Both forms of pointer dereference return a proxy object rather than a real reference, to ensure that the lock on the mutex is held across the assignment or method call, but this is transparent to the user.

Beyond Simple Accesses

The pointer-like semantics work very well for simple accesses such as assignment and calls to member functions. However, sometimes you need to perform an operation that requires multiple accesses under protection of the same lock, and that's what the update() method provides.

By calling update() you obtain an Updater object that holds a lock on the mutex protecting the data, and which can be used to access the protected data. The lock is held until the Updater object is destroyed, so you can safely perform multi-part operations. The Updater object also acts as a pointer-to-T, just like SynchronizedValue does, but this time the lock is already held. For example, the following function adds a trailing slash to a path held in a SynchronizedValue<std::string>. The use of the Updater object ensures that the string hasn't changed in between the query and the update.

?
1
2
3
4
5
6
7
8
9
void addTrailingSlashIfMissing(SynchronizedValue<std::string> & path)
{
     SynchronizedValue<std::string>::Updater u=path.update();
 
     if(u->empty() || (*u->rbegin()!='/'))
     {
         *u+='/';
     }
}

Operations Across Multiple Objects

Though SynchronizedValue<T> works very well for protecting a single object of type T, nothing that we've seen so far solves the problem of operations that require atomic access to multiple objects unless those objects can be combined within a single structure protected by a single mutex.

One way to protect access to two SynchronizedValue<T> objects is to construct a SynchronizedValue<T>::Updater for each object and use those to access the respective protected values; for instance:

?
1
2
3
4
5
6
7
8
9
10
11
12
SynchronizedValue<std::queue<MessageType> > q1,q2;
void transferMessage()
{
     SynchronizedValue<std::queue<MessageType> >::Updater u1=q1.update();
     SynchronizedValue<std::queue<MessageType> >::Updater u2=q2.update();
 
     if(!u1->empty())
     {
         u2->push_back(u1->front());
         u1->pop_front();
     }
}

This works well in some scenarios, but not all -- if the same two objects are updated together in different sections of code then you need to take care to ensure that the Updater objects are constructed in the same sequence in all cases, otherwise you have the potential for deadlock. This is just the same as when acquiring any two mutexes.


Ensuring that the correct mutex is locked when accessing data 

Under The Hood

We've seen how to use SynchronizedValue<T> to ensure that the right mutex is always locked when accessing an object; now it's time to look under the hood and see how it works.

It really is very simple: at its heart, a SynchronizedValue<T> is just a T object and a std::mutex. All the work is done by the proxy objects returned from the operators.

template<typename T>
class SynchronizedValue
{
    T data;
    std::mutex m;
public:
    DerefValue operator*();
    Updater operator->();
    Updater update();
};

Basic Pointer Operations

The basic pointer dereference operator returns an instance of the private nested class SynchronizedValue<T>::DerefValue:

    DerefValue operator*()
    {
        return DerefValue(*this);
    }

This class holds a std::unique_lock<std::mutex> which manages the lock and a reference to the protected T object. The lock is acquired when the object is constructed by the call to operator*, and released when the object is destroyed. DerefValue has a simple conversion operator to T which allows retrieval of the protected value, and also an assignment operator which allows the value to be set.

    operator T()
    {
        return data;
    }

    DerefValue& operator=(T const& newVal)
    {
        data=newVal;
        return *this;
    }

The arrow operator is implemented similarly, except this time it returns an instance of SynchronizedValue<T>::Updater. BecauseSynchronizedValue<T>::Updater also has an implementation of the arrow operator, the chaining rules for this operator mean that you can access members of the protected object directly using the normal pointer syntax, and the temporary Updater object holds the lock for you. Just like DerefValue, the Updater object acquires the lock in its constructor and releases it in its destructor.

This is the same Updater object we get from the update() member function, so it has a plain pointer dereference operator that returns a reference to the protected object in addition to the arrow operator that returns the pointer:

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

    T& operator*()
    {
        return data;
    }

Wrapping it Up

I've found classes like SynchronizedValue<T> invaluable in many projects, as it provides a simple way of ensuring that the right mutex is always held when accessing protected data. It's not a panacea, but those cases where it doesn't work likely require more careful thought anyway.

You can download the source code here, or from http://www.stdthread.co.uk/syncvalue if you wish to try it out. As written, it relies on C++0x facilities, but it should be easy to substitute an alternative mutex implementation if desired.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值