如何使用lock_guard在c++11中实现scoped_lock功能

How to implement scoped_lock functionality in c++11 using lock_guard

本文关键字:lock 实现 scoped 功能 何使用 guard c++11      更新时间:2023-10-16

看起来c++17中的scoped_lock提供了我想要的功能,但我目前与c++11绑定。

目前,当我们多次用同一个互斥对象调用guard_lock时,我看到了它的死锁问题。scoped_lock是否可以防止多次调用(即重新进入?(?在带有lock_guard的c++11中有这样做的最佳实践吗?

mutex lockingMutex;
void get(string s)
{
lock_guard<mutex> lock(lockingMutex);
if (isPresent(s))
{
//....
}
}
bool isPresent(string s)
{
bool ret = false;
lock_guard<mutex> lock(lockingMutex);
//....
return ret;
}

要能够多次锁定同一互斥对象,需要使用std::recursive_mutex。递归互斥比非递归互斥更昂贵。

不过,最好的做法是设计代码,使线程不会多次锁定同一互斥对象。例如,让公共函数先锁定互斥体,然后调用期望互斥体已经锁定的实现函数。实现函数不能调用锁定互斥对象的公共API函数。例如:

class A {
std::mutex m_;
int state_ = 0;
private: // These expect the mutex to have been locked.
void foo_() {
++state_;
}
void bar_() {
this->foo_();
}
public: // Public functions lock the mutex first.
void foo() {
std::lock_guard<std::mutex> lock(m_);
this->foo_();
}
void bar() {
std::lock_guard<std::mutex> lock(m_);
this->bar_();
}
};

作用域锁不能提供您想要的功能。

Scoped锁只是锁保护的一个变体。它之所以存在,是因为将锁保护更改为可变模板时存在一些ABI问题。

要拥有可重入互斥,您需要使用一个可重入的互斥。但这在运行时成本更高,而且通常表明您的互斥对象状态不小心。在持有互斥对象时,您应该完全理解正在执行的所有其他同步操作。

一旦您完全理解了正在执行的所有同步操作,就很容易避免递归锁定。

这里有两种模式可以考虑。首先,将公共锁定API与私有非锁定API分离。其次,将同步与实现分离。

private:
mutex lockingMutex;
bool isPresent(string s, lock_guard<mutex> const& lock) {
bool ret = false;
//....
return ret;
}
void get(string s, lock_guard<mutex> const& lock) {
if (isPresent(s, lock))
{
//....
}
}
public:  
void get(string s) {
return get( std::move(s), lock_guard<mutex>(lockingMutex) );
}
bool isPresent(string s) {
return isPresent( std::move(s), lock_guard<mutex>(lockingMutex) );
}
};

这里我用lock_guard<mutex>作为"我们有锁的证据"。

一个通常更好的选择是将类编写为非线程安全的,然后使用包装器:

template<class T>
struct mutex_guarded {
template<class T0, class...Ts,
std::enable_if_t<!std::is_same<std::decay_t<T0>, mutex_guarded>{}, bool> =true
>
mutex_guarded(T0&&t0, Ts&&...ts):
t( std::forward<T0>(t0), std::forward<Ts>(ts)... )
{}
mutex_guarded()=default;
~mutex_guarded=default;
template<class F>
auto read( F&& f ) const {
auto l = lock();
return f(t);
}
template<class F>
auto write( F&& f ) {
auto l = lock();
return f(t);
}
private:
auto lock() { return std::unique_lock<std::mutex>(m); }
auto lock() const { return std::unique_lock<std::mutex>(m); }
mutable std::mutex m;
T t;
};    

现在我们可以这样使用:

mutex_guarded<Foo> foo;
foo.write([&](auto&&foo){ foo.get("hello"); } );

您可以编写mutex_gaurdedshared_mutex_guardednot_mutex_guarded甚至async_guarded(它返回未来并序列化工作线程中的操作(。

只要类在方法中不离开自己的"控制区",这种模式就可以更容易地编写互斥体保护的数据,并且可以将相关的互斥体保护数据组合到一个包中,而不必重写它们。