C++ 我应该如何处理C++;?
根据设计,C++ 我应该如何处理C++;?,c++,mutex,move-constructor,C++,Mutex,Move Constructor,根据设计,std::mutex既不可移动也不可复制。这意味着持有互斥锁的类a将不会收到默认的移动构造函数 如何使此类型A以线程安全的方式移动?首先,如果要移动包含互斥对象的对象,您的设计肯定有问题 但如果您决定无论如何都要这样做,则必须在移动构造函数中创建一个新的互斥体,例如: // movable struct B{}; class A { B b; std::mutex m; public: A(A&& a) : b(std::mov
std::mutex
既不可移动也不可复制。这意味着持有互斥锁的类a
将不会收到默认的移动构造函数
如何使此类型
A
以线程安全的方式移动?首先,如果要移动包含互斥对象的对象,您的设计肯定有问题
但如果您决定无论如何都要这样做,则必须在移动构造函数中创建一个新的互斥体,例如:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
这是线程安全的,因为move构造函数可以安全地假设它的参数不在其他任何地方使用,因此不需要锁定参数。鉴于似乎没有一个好的、干净的、简单的方法来回答这个问题-Anton的解决方案我认为是正确的,但它确实值得商榷,除非有更好的答案,否则我建议将此类类放在堆上,并通过
std::unique\u ptr
:
auto a = std::make_unique<A>();
auto a=std::make_unique();
它现在是一种完全可移动的类型,任何在移动时锁定内部互斥锁的人仍然是安全的,即使这是否是一件好事仍有争议
如果需要复制语义,只需使用
auto a2 = std::make_shared<A>();
auto a2=std::make_shared();
< /代码> 使用互斥和C++移动语义是线程间安全高效传输数据的一种很好的方法。
想象一个“生产者”线程,它生成成批字符串并将它们提供给(一个或多个)消费者。这些批可以由包含(可能较大的)std::vector
对象的对象表示。
我们绝对希望将这些向量的内部状态“移动”到它们的消费者中,而不需要不必要的重复
您只需将互斥体识别为对象的一部分,而不是对象状态的一部分。也就是说,您不想移动互斥体
您需要什么样的锁定取决于您的算法、对象的通用性以及您允许的使用范围
如果您只从共享状态的“生产者”对象移动到线程本地的“消费”对象,那么您可以只锁定从中移动的对象
如果是更通用的设计,则需要同时锁定这两个选项。在这种情况下,您需要考虑死锁。< /P>
如果这是一个潜在问题,那么使用std::lock()
以无死锁的方式获取两个互斥锁上的锁
最后,您需要确保理解移动语义。
回想一下,“从中移动”对象处于有效但未知的状态。
不执行移动的线程在发现有效但未知的状态时,完全有可能有正当理由尝试访问移动自对象
再一次,我的制作人只是敲打琴弦,而消费者却拿走了全部的琴弦。在这种情况下,每次生产者尝试添加到向量时,都可能会发现向量非空或为空
简言之,如果对moved from对象的潜在并发访问相当于一次写入,那么这很可能是正常的。如果它相当于一次读取,那么考虑一下为什么读取任意状态是可以的。让我们从一段代码开始:
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:
...
请注意,我们必须首先默认构造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;
}
以下是正确保护上述场景的移动分配操作符:
// 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的要点是避免死锁
复制构造函数
您没有询问复制成员的情况,但我们不妨现在讨论他们(如果不是您,有人会需要他们)
复制构造函数与移动构造函数非常相似,只是使用了ReadLock
别名而不是WriteLock
。目前,这两个别名都是aliasstd::unique_lock
,因此没有任何区别
但在C++14中,您可以选择这样说:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
等
如果希望多个线程能够同时调用访问
状态的任何其他成员或自由函数,则也需要对其进行保护。例如,这里有交换:
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
来解决,而无需自检
更新
在下面的评论中,雅克对不得不在复制和移动构造函数中默认构造东西感到非常不高兴(他有一个观点)。如果你对这个问题有足够强烈的感觉,以至于你愿意花费大量的精力,你可以这样避免它:
- 添加您需要的任何锁类型作为数据成员。这些成员必须位于受保护的数据之前
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_);
}
}
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();
}
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
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)};
}
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
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);
}