将互斥锁与其数据相关联的正确方法是什么?
What's the proper way to associate a mutex with its data?
在将资金从一个银行账户转移到另一个银行账户的经典问题中,公认的解决方案(我相信)是将一个互斥锁与每个银行账户关联,然后在从一个账户提取资金并将其存入另一个账户之前将其锁定。乍一看,我会这样做:
class Account {
public:
void deposit(const Money& amount);
void withdraw(const Money& amount);
void lock() { m.lock(); }
void unlock() { m.unlock(); }
private:
std::mutex m;
};
void transfer(Account& src, Account& dest, const Money& amount)
{
src.lock();
dest.lock();
src.withdraw(amount);
dest.deposit(amount);
dest.unlock();
src.unlock();
}
但是手动解锁有气味。我可以将互斥锁设为公共,然后在transfer
中使用std::lock_guard
,但是公共数据成员也有问题。
对std::lock_guard
的要求是其类型满足BasicLockable要求,也就是说对lock
和unlock
的调用是有效的。Account
满足这个要求,所以我可以直接使用std::lock_guard
和Account
:
void transfer(Account& src, Account& dest, const Money& amount)
{
std::lock_guard<Account> g1(src);
std::lock_guard<Account> g2(dest);
src.withdraw(amount);
dest.deposit(amount);
}
这看起来不错,但是我以前从来没有见过这样的事情,而且在Account
中复制互斥锁的锁定和解锁本身就有点臭。
更新:在下面的评论中,我注意到std::lock
可以用来避免死锁,但我忽略了std::lock
依赖于try_lock
功能的存在(除了lock
和unlock
)。将try_lock
添加到Account
的界面似乎是一个相当粗糙的hack。这样看来,如果Account
对象的互斥锁要留在Account
中,它必须是公共的。有股臭味。
一些建议的解决方案让客户端使用包装类来静默地将互斥锁与Account
对象关联起来,但是,正如我在评论中指出的那样,这似乎使代码的不同部分很容易在Account
周围使用不同的包装对象,每个都创建自己的互斥锁,这意味着代码的不同部分可能会尝试使用不同的互斥锁来锁定Account
。这是不好的。
其他建议的解决方案依赖于一次只锁定一个互斥锁。这样就不需要锁定多个互斥锁,但代价是某些线程可能会看到不一致的系统视图。实质上,这放弃了涉及多个对象的操作的事务语义。
在这一点上,公共互斥锁开始看起来像是可用选项中最不糟糕的,这是我真的不想得出的结论。真的没有更好的了吗?查看Herb Sutter在 c++ and Beyond 2012: c++ Concurrency的演讲。他展示了在c++ 11中实现类似Monitor object的例子。
monitor<Account> m[2];
transaction([](Account &x,Account &y)
{
// Both accounts are automaticaly locked at this place.
// Do whatever operations you want to do on them.
x.money-=100;
y.money+=100;
},m[0],m[1]);
// transaction - is variadic function template, it may accept many accounts
实现:现场演示
#include <iostream>
#include <utility>
#include <ostream>
#include <mutex>
using namespace std;
typedef int Money;
struct Account
{
Money money = 1000;
// ...
};
template<typename T>
T &lvalue(T &&t)
{
return t;
}
template<typename T>
class monitor
{
mutable mutex m;
mutable T t;
public:
template<typename F>
auto operator()(F f) const -> decltype(f(t))
{
return lock_guard<mutex>(m),
f(t);
}
template<typename F,typename ...Ts> friend
auto transaction(F f,const monitor<Ts>& ...ms) ->
decltype(f(ms.t ...))
{
return lock(lvalue(unique_lock<mutex>(ms.m,defer_lock))...),
f(ms.t ...);
}
};
int main()
{
monitor<Account> m[2];
transaction([](Account &x,Account &y)
{
x.money-=100;
y.money+=100;
},m[0],m[1]);
for(auto &&t : m)
cout << t([](Account &x){return x.money;}) << endl;
}
输出是:
900
1100
暂时把钱"运走"并没有什么错。像这样:
Account src, dst;
dst.deposit(src.withdraw(400));
现在让每个方法都是线程安全的,例如
int Account::withdraw(int n)
{
std::lock_guard<std::mutex> _(m_);
balance -= n;
return n;
}
我更喜欢使用非侵入式包装器类,而不是用互斥锁污染原始对象,并在每次方法调用时将其锁定。这个包装器类(我将其命名为Protected<T>
)包含作为私有变量的用户对象。Protected<T>
授予另一个类Locker<T>
友谊。锁将包装器作为其构造函数参数,并为用户对象提供公共访问器方法。该锁还使包装器的互斥锁在其生命周期内保持锁定状态。因此,锁存器的生命周期定义了一个作用域,在这个作用域中,可以以安全的方式访问原始对象。
Protected<T>
可以实现operator->
,实现快速调用单个方法。
工作的例子:
#include <iostream>
#include <mutex>
template<typename>
struct Locker;
template<typename T>
struct Protected
{
template<typename ...Args>
Protected(Args && ...args) :
obj_(std::forward<Args>(args)...)
{
}
Locker<const T> operator->() const;
Locker<T> operator->();
private:
friend class Locker<T>;
friend class Locker<const T>;
mutable std::mutex mtx_;
T obj_;
};
template<typename T>
struct Locker
{
Locker(Protected<T> & p) :
lock_(p.mtx_),
obj_(p.obj_)
{
std::cout << "LOCK" << std::endl;
}
Locker(Locker<T> && rhs) = default;
~Locker()
{
std::cout << "UNLOCKn" << std::endl;
}
const T& get() const { return obj_; }
T& get() { return obj_; }
const T* operator->() const { return &get(); }
T* operator->() { return &get(); }
private:
std::unique_lock<std::mutex> lock_;
T & obj_;
};
template<typename T>
struct Locker<const T>
{
Locker(const Protected<T> & p) :
lock_(p.mtx_),
obj_(p.obj_)
{
std::cout << "LOCK (const)" << std::endl;
}
Locker(Locker<const T> && rhs) = default;
~Locker()
{
std::cout << "UNLOCK (const)n" << std::endl;
}
const T& get() const { return obj_; }
const T* operator->() const { return &get(); }
private:
std::unique_lock<std::mutex> lock_;
const T & obj_;
};
template<typename T>
Locker<T> Protected<T>::operator->()
{
return Locker<T>(const_cast<Protected<T>&>(*this));
}
template<typename T>
Locker<const T> Protected<T>::operator->() const
{
return Locker<T>(const_cast<Protected<T>&>(*this));
}
struct Foo
{
void bar() { std::cout << "Foo::bar()" << std::endl; }
void car() const { std::cout << "Foo::car() const" << std::endl; }
};
int main()
{
Protected<Foo> foo;
// Using Locker<T> for rw access
{
Locker<Foo> locker(foo);
Foo & foo = locker.get();
foo.bar();
foo.car();
}
// Using Locker<const T> for const access
{
Locker<const Foo> locker(foo);
const Foo & foo = locker.get();
foo.car();
}
// Single actions can be performed quickly with operator->
foo->bar();
foo->car();
}
生成以下输出:
LOCK
Foo::bar()
Foo::car() const
UNLOCK
LOCK (const)
Foo::car() const
UNLOCK (const)
LOCK
Foo::bar()
UNLOCK
LOCK
Foo::car() const
UNLOCK
使用在线编译器测试。
更新:修复const正确性。
PS:还有一个异步的变体
我个人是LockingPtr范例的粉丝(这篇文章相当过时,我个人不会遵循它的所有建议):
struct thread_safe_account_pointer {
thread_safe_account_pointer( std::mutex & m,Account * acc) : _acc(acc),_lock(m) {}
Account * operator->() const {return _acc;}
Account& operator*() const {return *_acc;}
private:
Account * _acc;
std::lock_guard<std::mutex> _lock;
};
并实现包含Account
对象的类,像这样:
class SomeTypeWhichOwnsAnAccount {
public:
thread_safe_account_pointer get_and_lock_account() const {return thread_safe_account_pointer(mutex,&_impl);}
//Optional non thread-safe
Account* get_account() const {return &_impl;}
//Other stuff..
private:
Account _impl;
std::mutex mutex;
};
如果合适的话,可以用智能指针替换指针,并且您可能需要一个const_thread_safe_account_pointer
(或者更好的是一个通用模板thread_safe_pointer
类)
为什么这比显示器(IMO)好?
- 你可以设计Account类而不用考虑线程安全;线程安全是使用类的对象的属性,而不是类本身的属性。
- 在类中嵌套调用成员函数时不需要递归互斥体。
- 您可以在代码中清楚地记录您是否锁定互斥锁(并且您可以通过不实现
get_account
来防止完全使用无锁定)。同时拥有get_and_lock()
和get()
函数迫使考虑线程安全性。 - 当定义函数(全局或成员)时,你有一个清晰的语义来指定一个函数是需要对象的互斥锁(只需传递一个
thread_safe_pointer
)还是线程安全不可知(使用Account&
)。 - 最后但并非最不重要的是,
thread_safe_pointer
具有与监视器完全不同的语义:
考虑一个MyVector
类,它通过监视器实现线程安全,以及以下代码:
MyVector foo;
// Stuff.. , other threads are using foo now, pushing and popping elements
int size = foo.size();
for (int i=0;i < size;++i)
do_something(foo[i]);
在我看来,像这样的代码真的很糟糕,因为它让你觉得safe
认为监视器会为你照顾线程安全,而这里我们有一个难以发现的竞争条件。您的问题是将锁定与数据关联起来。在我看来,将mutex
填充到对象中是好的。您还可以更进一步,将对象本质上变成监视器:进入函数成员时锁定,离开时解锁。
我认为为每个帐户提供自己的锁是好的。它向代码的任何读者提供了一个明确的信号,即访问Account
是一个临界区。
任何涉及每个帐户一个锁的解决方案的缺点是,当您编写同时操作多个帐户的代码时,您必须注意死锁。但是,避免这个问题的直接方法是一次限制与一个帐户的交互。这不仅避免了潜在的死锁问题,还增加了并发性,因为当当前线程忙于其他事情时,您没有阻止其他线程访问其他帐户。
您对一致性视图的关注是有效的,但是可以通过记录当前事务中发生的操作来实现。例如,你可以用一个事务日志装饰你的deposit()
和withdraw()
操作。
class Account {
void deposit(const Money &amount);
void withdraw(const Money &amount);
public:
void deposit(const Money &amount, Transaction& t) {
std::lock_guard<std::mutex> _(m_);
deposit(amount);
t.log_deposit(*this, amount);
}
void withdraw(const Money &amount, Transaction& t) {
std::lock_guard<std::mutex> _(m_);
withdraw(amount);
t.log_withdraw(*this, amount);
}
private:
std::mutex m_;
};
那么,transfer
是一个记录的取款和存款。
void transfer (Account &src, Account &dest, const Money &amount,
Transaction &t) {
t.log_transfer(src, dest, amount);
try {
src.withdraw(amount, t);
dest.deposit(amount, t);
t.log_transfer_complete(src, dest, amount);
} catch (...) {
t.log_transfer_fail(src, dest, amount);
//...
}
}
注意,事务日志的概念与您选择如何部署锁是正交的。
我认为您的答案是按照您的建议使用std::lock(),但将其放入好友函数中。这样就不需要将帐户互斥锁设为公共。新的好友函数不使用deposit()和withdraw()函数,需要分别锁定和解锁互斥锁。请记住,友元函数不是成员函数,但可以访问私有成员。
typedef int Money;
class Account {
public:
Account(Money amount) : balance(amount)
{
}
void deposit(const Money& amount);
bool withdraw(const Money& amount);
friend bool transfer(Account& src, Account& dest, const Money& amount)
{
std::unique_lock<std::mutex> src_lock(src.m, std::defer_lock);
std::unique_lock<std::mutex> dest_lock(dest.m, std::defer_lock);
std::lock(src_lock, dest_lock);
if(src.balance >= amount)
{
src.balance -= amount;
dest.balance += amount;
return true;
}
return false;
}
private:
std::mutex m;
Money balance;
};
大多数解决方案都存在一个问题,即数据是公开的,因此可以在不锁定锁的情况下访问它。
有一种方法可以解决这个问题,但是你不能使用模板,因此必须求助于宏。在c++ 11中实现它要好得多,而不是在这里重复整个讨论,我链接到我的实现:https://github.com/sveljko/lockstrap
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 实现无开销push_back的最佳方法是什么
- C++从另一个类访问公共静态向量的正确方法是什么
- 在 c++ 中拥有一组结构的正确方法是什么?
- 通过JNI传递数据数组的最快方法是什么
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 使用不同的CRT将新的C++代码与旧的(二进制)组件隔离开来的最佳方法是什么
- 当无法使用模板和宏时,生成类型变体C++代码的最简单方法是什么?
- 在另一个类视图中添加最多2个图表的正确方法是什么
- 在C++中样板"冷/never_inline"错误处理技术的最佳方法是什么?
- 在 c++ 中对类中的 c 字符串动态数组进行排序的最佳方法是什么?
- 在C++中包含原型文件的正确方法是什么?
- 在 OpenCV C++ 中估计基本矩阵之前对相应点进行归一化的正确方法是什么?
- 在PostgreSQL中根据它们的ID选择大量行的最快方法是什么?
- 在OSX上使用CMake将Adobe的XMP工具包构建为共享库的最简单方法是什么?
- 将一系列整数放入类的最佳方法是什么?
- 从长整整转换为uint64_t的推荐方法是什么?
- 将此布尔值传递给此函数的最有效方法是什么?
- 通过比较C++中的行在 txt 文件中搜索的最简单方法是什么?