按照设计,std::mutex
不可移动也不可复制。这意味着一个拥有互斥锁的类 A
将不会接收默认移动构造函数。
我该如何以线程安全的方式使这种类型 A
可移动?
按照设计,std::mutex
不可移动也不可复制。这意味着一个拥有互斥锁的类 A
将不会接收默认移动构造函数。
我该如何以线程安全的方式使这种类型 A
可移动?
我们从一些代码开始:
class A
{
using MutexType = std::mutex;
using ReadLock = std::unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType mut_;
std::string field1_;
std::string field2_;
public:
...
我在里面放了一些相当暗示性的类型别名,虽然我们在C++11中不会真正利用它们,但在C++14中它们变得更加有用。耐心等待,我们会到那里的。
你的问题可以归结为:
如何为这个类编写移动构造函数和移动赋值运算符?
我们将从移动构造函数开始。
移动构造函数
请注意,成员mutex
已经被设置为mutable
。严格来说,在移动成员中这并非必需,但我假设你还想要复制成员。如果不是这样的话,则没有必要使互斥量成为mutable
。
在构造A
时,您不需要锁定this->mut_
。但您需要锁定正在构造的对象(移动或复制)的mut_
。可以这样做:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
请注意,我们首先必须默认构造this
的成员,然后仅在a.mut_
加锁后再为其赋值。
移动赋值
移动赋值运算符要复杂得多,因为您不知道其他某个线程是否正在访问赋值表达式的lhs或rhs。 一般而言,您需要防范以下情况:
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
这里是正确保护上述场景的移动赋值运算符:
A& operator=(A&& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
WriteLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
return *this;
}
请注意,必须使用std::lock(m1, m2)
来锁定这两个互斥量,而不是只是一个接一个地锁定它们。如果您一个接一个地锁定它们,那么当两个线程按照上面显示的相反顺序分配两个对象时,您可能会遇到死锁。 std::lock
的作用是避免这种死锁。
拷贝构造函数
虽然你没有询问拷贝成员,但我们也可以现在谈论它们(如果不是你,总有人会需要它们)。
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
复制构造函数看起来与移动构造函数很相似,只是使用了ReadLock
别名而不是WriteLock
。目前这两个别名都是指向std::unique_lock<std::mutex>
,因此并没有什么区别。
但在C++14中,您将有以下选项:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
这 可能 是一种优化方式,但并不确定。你需要进行测量来确定是否是优化。但是通过此更改,可以在多个线程中同时从相同的 rhs 进行复制构造。C++11 解决方案会强制您使这些线程顺序执行,即使 rhs 没有被修改。
复制分配
为了完整起见,这里是复制分配运算符,阅读其他内容后应该相当容易理解:
A& operator=(const A& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
ReadLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = a.field1_;
field2_ = a.field2_;
}
return *this;
}
等等。
如果您希望多个线程能够同时调用访问A状态的任何其他成员或自由函数,那么这些函数也需要受到保护。例如,这里是swap
:
friend void swap(A& x, A& y)
{
if (&x != &y)
{
WriteLock lhs_lk(x.mut_, std::defer_lock);
WriteLock rhs_lk(y.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
using std::swap;
swap(x.field1_, y.field1_);
swap(x.field2_, y.field2_);
}
}
请注意,如果仅依赖于std::swap
,则锁定粒度将不正确,在std::swap
内部执行的三个移动之间进行锁定和解锁。swap
可以让你了解到为“线程安全”的A
提供的API可能需要与“非线程安全”的API不同,因为存在“锁定粒度”问题。MutexType
使用std::recursive_mutex
来解决而不进行自检。mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
然后在构造函数中(例如复制构造函数)执行以下操作:A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
哎呀,Yakk在我完成这个更新之前把他的评论删掉了。但是他应该得到推动这个问题并将解决方案纳入这个答案的功劳。
更新2
dyp提出了这个好建议:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
鉴于目前似乎没有一种好的、干净的、简单的方法来回答这个问题——我认为Anton的解决方案是正确的,但这肯定是有争议的,除非有更好的答案出现,否则我建议将这样的类放在堆上,并通过 std::unique_ptr
进行管理:
auto a = std::make_unique<A>();
现在它是全可移动类型,任何在移动发生时锁定内部互斥锁的人仍然是安全的,尽管这是否是一个好做法还有争议
如果你需要复制语义,只需使用
auto a2 = std::make_shared<A>();
template<class T>
struct synchronized {
template<class F>
auto read(F&& f) const&->std::result_of_t<F(T const&)> {
return access(std::forward<F>(f), *this);
}
template<class F>
auto read(F&& f) &&->std::result_of_t<F(T&&)> {
return access(std::forward<F>(f), std::move(*this));
}
template<class F>
auto write(F&& f)->std::result_of_t<F(T&)> {
return access(std::forward<F>(f), *this);
}
// uses `const` ness of Syncs to determine access:
template<class F, class... Syncs>
friend auto access( F&& f, Syncs&&... syncs )->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
};
synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}
// special member functions:
synchronized( T & o ):t(o) {}
synchronized( T const& o ):t(o) {}
synchronized( T && o ):t(std::move(o)) {}
synchronized( T const&& o ):t(std::move(o)) {}
synchronized& operator=(T const& o) {
write([&](T& t){
t=o;
});
return *this;
}
synchronized& operator=(T && o) {
write([&](T& t){
t=std::move(o);
});
return *this;
}
private:
template<class X, class S>
static auto smart_lock(S const& s) {
return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class X, class S>
static auto smart_lock(S& s) {
return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class L>
static void lock(L& lockable) {
lockable.lock();
}
template<class...Ls>
static void lock(Ls&... lockable) {
std::lock( lockable... );
}
template<size_t...Is, class F, class...Syncs>
friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
lock( std::get<Is>(locks)... );
return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
}
mutable std::shared_timed_mutex m;
T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
return {std::forward<T>(t)};
}
包含C++14和C++1z的特性。
这假设const
操作是多读安全的(这是std
容器所假定的)。
使用方法如下:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
针对具有同步访问的int
,我建议不要使用synchronized(synchronized const&)
。这很少需要。
如果您需要synchronized(synchronized const&)
,我倾向于使用std::aligned_storage
替换T t;
,允许手动放置构造,并进行手动销毁。这允许正确的生命周期管理。
除此之外,我们可以复制源T
,然后从中读取:
synchronized(synchronized const& o):
t(o.read(
[](T const&o){return o;})
)
{}
synchronized(synchronized && o):
t(std::move(o).read(
[](T&&o){return std::move(o);})
)
{}
用于赋值:
synchronized& operator=(synchronized const& o) {
access([](T& lhs, T const& rhs){
lhs = rhs;
}, *this, o);
return *this;
}
synchronized& operator=(synchronized && o) {
access([](T& lhs, T&& rhs){
lhs = std::move(rhs);
}, *this, std::move(o));
return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
access([](T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
}, *this, o);
}
synchronized
对象(相同类型)的n元任意操作可以一起工作,而无需事先硬编码。添加友元声明并且多种类型的n元对象可能会一起工作。在这种情况下,我可能不得不将移出成为内联朋友,以处理重载冲突。
{{链接1:实时示例}}
首先,如果你想移动一个包含互斥锁的对象,那么你的设计肯定有问题。
但如果你决定这样做,你需要在移动构造函数中创建一个新的互斥锁,例如:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
这是线程安全的,因为移动构造函数可以安全地假设其参数不会在任何其他地方使用,因此不需要锁定参数。
a.mutex
被锁定,你会失去那个状态。-1 - user2249683std::unique_ptr
中 - 这似乎更简洁,不太可能导致混淆问题。好问题。 - Mike Vinestd::vector<std::string>
对象的对象表示。
我们绝对希望将这些向量的内部状态“移动”到它们的消费者,而不需要不必要的复制。std::lock()
以无死锁方式获取两个互斥锁。
http://en.cppreference.com/w/cpp/thread/lock
作为最后的说明,您需要确保理解移动语义。 请注意,被移动的对象处于有效但未知状态。 完全有可能,当没有执行移动的线程尝试访问已被移动的对象时,它可能会发现该对象处于有效但未知状态。
std::lock_guard
是方法作用域的。 - Cory Kramer