我应该如何处理C ++中的可移动types的互斥体?

按照devise, std::mutex不可移动也不可复制。 这意味着一个持有互斥体的类A将不会收到默认的移动构造函数。

我将如何使这种typesA可以线程安全的方式移动?

让我们从一些代码开始:

 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: ... 

我已经在那里放了一些颇具启发性的types别名,我们不会真的在C ++ 11中占据优势,但在C ++ 14中变得更有用。 要有耐心,我们会到达那里。

你的问题归结为:

我如何编写移动构造函数并为此类移动赋值运算符?

我们将从移动构造函数开始。

移动构造函数

请注意,成员mutex已经变得mutable 。 严格来说这对于移动会员来说不是必要的,但是我假设你也想要复制会员。 如果情况并非如此,则不需要使互斥变得mutable

构buildA ,不需要lockingthis->mut_ 。 但是你需要locking你正在构build的对象的mut_ (移动或复制)。 这可以这样做:

  A(A&& a) { WriteLock rhs_lk(a.mut_); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); } 

请注意,我们必须首先构造这个成员,然后仅在a.mut_被locking后才a.mut_

移动分配

移动赋值运算符要复杂得多,因为您不知道其他线程是否正在访问赋值expression式的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)来locking两个互斥锁,而不是一个接一个地locking它们。 如果一个接一个地locking它们,那么当两个线程如上所示以相反的顺序分配两个对象时,可能会发生死锁。 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中,你可以select这样说:

  using MutexType = std::shared_timed_mutex; using ReadLock = std::shared_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>; 

可能是一个优化,但不是绝对的。 你将不得不测量,以确定是否是。 但是,通过这种改变,可以同时在多个线程中相同的rhs复制构造。 即使rhs没有被修改,C ++ 11解决scheme也会强制你使这样的线程顺序进行。

复制分配

为了完整起见,这里是复制赋值操作符,在阅读其他所有内容后,应该相当自我解释:

  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来完成这个工作,那么locking将会处于错误的粒度,在std::swap将在内部执行的三个移动之间进行locking和解锁。

事实上,思考swap可以让你深入了解你可能需要为“线程安全” A提供的API,因为“locking粒度”通常会与“非线程安全”API不同,问题。

还要注意防止“自交换”的必要性。 “自交换”应该是没有操作的。 如果没有自检,则会recursion地locking相同的互斥锁。 这也可以通过使用std::recursive_mutex for MutexTypeMutexType

更新

在下面的评论中,Yakk非常不满意在复制和移动构造函数中默认构造东西(他有一个观点)。 如果你对这个问题感觉足够强烈,以至于你愿意在它上面花费记忆,你可以避免这样做:

  • 添加您需要的任何lockingtypes作为数据成员。 这些成员必须在受保护的数据之前:

     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删除了他的评论。 但是他推动这个问题值得赞扬,并得到了解决scheme。

更新2

dyp提出了这个好build议:

  A(const A& a) : A(a, ReadLock(a.mut_)) {} private: A(const A& a, ReadLock rhs_lk) : field1_(a.field1_) , field2_(a.field2_) {} 

这是一个颠倒的答案。 而不是embedded“这个对象需要同步”作为types的基础,而是将其注入到任何types下。

你处理一个同步的对象非常不同。 一个大问题是你不得不担心死锁(locking多个对象)。 它也基本上不应该是你的“对象的默认版本”:同步的对象是争用的对象,你的目标应该是尽量减less线程之间的争用,而不是在地毯下扫描。

但同步对象仍然有用。 我们可以编写一个同步包装任意types的类,而不是从同步器inheritance。 用户不得不跳过一些箍环来对对象进行同步操作,但是它们并不局限于对象的一些手动编码的有限操作集合。 他们可以将多个操作组合成一个对象,或者对多个对象进行操作。

这是围绕任意typesT的同步包装:

 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 >(sm, X{}); } template<class X, class S> static auto smart_lock(S& s) { return std::unique_lock< std::shared_timed_mutex >(sm, 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 ++ 1zfunction。

这假定const操作是多读者安全的(这是std容器假定的)。

使用看起来像:

 synchronized<int> x = 7; x.read([&](auto&& v){ std::cout << v << '\n'; }); 

对于具有同步访问的int

我build议不要synchronized(synchronized const&) 。 这是很less需要的。

如果你需要synchronized(synchronized const&) ,我会试图取代T t;std::aligned_storage ,允许手动放置build设,并做手动销毁。 这允许正确的终身pipe理。

除此之外,我们可以复制源代码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); } 

放置和alignment的存储版本有点混乱。 大部分对t访问将被成员函数T&t()T const&t()const所取代,除非在构造中你必须跳过一些循环。

通过synchronized一个包装而不是类的一部分,所有我们必须确保的是,类内部尊重const作为多读者,并写在一个单线程的方式。

极less数情况下,我们需要一个同步的实例,我们像上面那样跳过。

在上面的任何错别字道歉。 有可能是一些。

上述的一个好处是对synchronized对象(相同types)的任意操作可以一起工作,而不需要事先对其进行硬编码。 添加一个朋友声明和n元多种types的synchronized对象可能会一起工作。 在这种情况下,我可能不得不把access从一个内联的朋友处理,以解决超负荷的问题。

活的例子

鉴于似乎没有一个很好,干净,简单的方法来回答这个问题 – 安东的解决scheme,我认为是正确的,但它肯定是有争议的,除非有更好的答案出现,我会build议把这样一个类堆在一起,照顾它通过一个std::unique_ptr

 auto a = std::make_unique<A>(); 

它现在是一个完全可移动的types,任何人在发生移动时都对内部互斥锁有locking,即使可以辩护是否是件好事

如果你需要复制语义只是使用

 auto a2 = std::make_shared<A>(); 

使用互斥锁和C ++移动语义是在线程之间安全高效地传输数据的绝佳方式。

设想一个“生产者”线程,可以批量生成string并将其提供给(一个或多个)消费者。 这些批可以由包含(可能很大) std::vector<std::string>对象的对象表示。 我们绝对想把这些载体的内部状态“移”到消费者身上,而不是不必要的重复。

您只需将互斥锁识别为不属于对象状态的对象的一部分。 也就是说,你不想移动互斥锁。

你需要什么locking取决于你的algorithm,或者你的对象有多普遍,以及你允许的用途。

如果你只是从一个共享的状态'生产者'对象移动到一个线程本地'消费'对象,你可能会确定只locking移动对象。

如果这是一个更一般的devise,你将需要同时locking。 在这种情况下,你需要考虑死锁。

如果这是潜在的问题,那么使用std::lock()以无死锁方式获取这两个互斥锁上的锁。

http://en.cppreference.com/w/cpp/thread/lock

作为最后的注意事项,您需要确保您了解移动语义。 回想一下,从对象的移动是有效的,但未知的状态。 完全可能的是,一个没有执行移动的线程有一个合理的理由来尝试访问从移动的对象,当它可能find有效但未知的状态。

我的制作人又一次把弦串起来了,消费者正在把整个负担都拿走。 在这种情况下,每当生产者试图添加到vector中,它可能会发现vector非空或空。

简而言之,如果从对象移出的潜在并发访问相当于写入,则可能是正确的。 如果它相当于一个阅读,那么想想为什么阅读一个任意的状态是可以的。

首先,如果要移动包含互斥锁的对象,则必须在devise中出现问题。

但是如果你决定这样做,你必须在移动构造函数中创build一个新的互斥体,例如:

 // movable struct B{}; class A { B b; std::mutex m; public: A(A&& a) : b(std::move(ab)) // m is default-initialized. { } }; 

这是线程安全的,因为移动构造函数可以安全地假定它的参数没有在其他地方使用,所以不需要locking参数。