为什么没有在Boost中使用Execute-Around Idiom作为线程安全访问对象的智能指针?

时间:2016-08-02 21:46:51

标签: c++ multithreading c++11 boost concurrency

为什么没有在Boost-library中使用Execute-Around Pointer Idiom作为线程安全访问对象的智能指针?

众所周知,Execute-Around Pointer Idiom:https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Execute-Around_Pointer

Execute-Around Pointer Idiom的主要思想 - 我们没有返回引用或指向类成员的指针,但是我们返回一个类型为proxy的临时对象:http://ideone.com/cLS8Ph

  • 在访问指向类成员proxy (T * const _p, mutex_type& _mtx) : p(_p), lock(_mtx) {}
  • 的指针之前创建的临时对象
  • 然后proxy可以访问指向类成员T* operator -> () {return p;}
  • 的指针
  • 并在完成整个表达式后销毁:完成所有函数后,其中成员用作参数,并在其他计算后~proxy () {}

这就是为什么这段代码是线程安全的:

execute_around<std::vector<int>> vecc(10, 10);
...
int res = std::sort(vecc->begin(), vecc->end()); // thread-safe in all threads

我们可以使用这个成语,就像智能​​指针一样,在我们访问成员变量或函数之前锁定互斥锁并在之后解锁互斥锁。这样做总是只会锁定与此对象相关的互斥锁。

http://ideone.com/kB3wnu

#include <iostream>
#include <thread>
#include <mutex>
#include <memory>
#include <vector>
#include <numeric>
#include <algorithm>

template<typename T, typename mutex_type = std::recursive_mutex>
class execute_around {
  std::shared_ptr<mutex_type> mtx;
  std::shared_ptr<T> p;

  void lock() const { mtx->lock(); }
  void unlock() const { mtx->unlock(); }
  public:
    class proxy {
      std::unique_lock<mutex_type> lock;
      T *const p;
      public:
        proxy (T * const _p, mutex_type& _mtx) : p(_p), lock(_mtx) {} 
        T* operator -> () {return p;}
        const T* operator -> () const {return p;}
    };

    template<typename ...Args>
    execute_around (Args ... args) : 
        p(std::make_shared<T>(args...)), mtx(std::make_shared<mutex_type>()) {}  

    proxy operator -> () { return proxy(p.get(), *mtx); }
    const proxy operator -> () const { return proxy(p.get(), *mtx); }
    template<class... Args> friend class std::lock_guard;
};

void thread_func(execute_around<std::vector<int>> vecc) 
{
  vecc->push_back(100); // thread-safe  
  int res = std::accumulate(vecc->begin(), vecc->end(), 0); // thread-safe
  std::cout << std::string("res = " + std::to_string(res) + "\n");

  { //all the following code in this scope is thread safe
    std::lock_guard<decltype(vecc)> lock(vecc);
    auto it = std::find(vecc->begin(), vecc->end(), 100);
    if(it != vecc->end()) std::cout << *it << std::endl;
  }
}

int main()
{
  execute_around<std::vector<int>> vecc(10, 10);

  auto copied_vecc_ptr = vecc; // copy-constructor

  std::thread t1([&]() { thread_func(copied_vecc_ptr); });
  std::thread t2([&]() { thread_func(copied_vecc_ptr); });
  t1.join(); t2.join();

  return 0;
}

输出:

res = 200
100
res = 300
100

我们可以将execute_around用于任何类型,任何互斥锁以及添加为friend的任何锁定,并具有以下几个功能:

优于标准的优势 std::recursive_mutex

  • 如果没有锁定互斥锁,您将无法访问对象成员,不会忘记 - 自动完成
  • 您无法选择保护其他对象或代码段的错误互斥锁

其他功能:

  • 访问
  • 后,您不会忘记解锁互斥锁
  • 如果你知道这个对象可以在多个线程中使用,那么没有人可以在不锁定互斥锁的情况下访问它 - execute_around保证这一点(但另外你可以将它与其他互斥体一起使用)保护整个代码部分,而不仅仅是一个对象)
  • 你可以将对象的成员作为参数传递给函数,这在函数的整个执行过程中将是线程安全的 - 正如我们对std::accumulate()所做的那样
  • 当我们访问多个成员(变量和函数)并在单个表达式中执行多个锁时,我们不会遇到死锁 - 如果我们使用std::recursive_mutex
  • 它没有operator *那么你不能对对象进行线程不安全的引用,但可以得到对象成员的不安全引用
  • 它有copy-constructor,但没有赋值 - operator =
  • 它可能有许多副本,指向单个对象&amp;互斥

可能出现的问题

在某些情况下,我们应该使用executive_around作为标准std::mutex,即使用lock_guard,但如果我们忘记了这个(std::lock_guard<decltype(vecc)> lock(vecc);),那么我们就会遇到问题:

  • 我们可以引用对象的成员,然后使用它thread-unsafe
  • 我们可以得到这个对象的迭代器,后来使用它不安全,也可以被其他线程无效

是否有任何其他可能的问题解释为什么不使用Boost中的Execute-Around Idiom作为线程安全访问对象的智能指针?

即。 executive_around有哪些其他问题,但标准的互斥锁和锁没有这些问题?

行为proxy类:Temporary object lifetime

2016-07-12编程语言C ++标准工作草案:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf

  

12.2临时物品

     

§12.26第三个上下文是指引用绑定到a   temporary.115引用绑定的临时或   临时的,是子对象的完整对象   引用的约束持续存在于引用的生命周期中,除了:

     

(6.1)......

     

(6.2) - 临时绑定到a中返回值的生命周期   函数返回语句(6.6.3)未扩展;暂时的是   在return语句中的完整表达结束时被销毁。

     

(6.3)......

     

一个临时的破坏,其寿命不会延长   绑定到引用在每个被破坏之前被排序   在同一个完整表达式中较早构造的临时表。如果   两个或多个临界参与者的生命周期   在同一点结束,这些临时工作在那时被摧毁   按照完工建设的相反顺序。

1 个答案:

答案 0 :(得分:3)

通常,基于互斥锁的线程安全性不构成。

即如果操作A是线程安全的,并且操作B是线程安全的,则操作A和操作B不是。

因此,你不能“闯入并忘记”。您必须意识到您正在进行基于互斥的操作,这使得执行的透明性变得危险。

例如,假设您有一个线程安全容器。

你这样做:

std::vector<Element> dest;
std::copy_if( c->begin(), c->end(), std::back_inserter(dest),
[&](auto&& e){
  this->ShouldCopy(e);
});

看起来安全,不是吗?我们将容器c从普通容器智能指针升级到执行周围的智能指针,现在它在访问之前锁定c

一切都很好。

但事实并非如此。如果this->ShouldCopy(Element const&)获取任何互斥锁(例如bob),我们刚刚创建了一个潜在的死锁。

如果bobc内的互斥锁之前锁定在其他位置,则两个线程都可以锁定并永远饿死。

这可能是非确定性的,并且它不依赖于所讨论代码的本地正确性(至少在C ++中)。您只能通过全局代码分析来发现它。

在这种情况下,mutex上的锁定透明度可以使代码更少安全,而不是更明显我们获得互斥锁。因为至少如果它显而易见且昂贵,它可能更加孤立,更容易追踪。

这也是为什么有些人认为递归互斥体是反模式的原因:如果你对使用互斥锁的控制很少,你无法阻止递归,你的代码可能不会管理全局顺序所有的互斥体。

此外,保护shared_ptr单独的互斥锁的内容是愚蠢的。将互斥锁和对象存储在同一个结构中,不要破坏局部性。

话虽如此,我确实使用了你所写的变体。

template<class T>
struct mutex_guarded {
  template<class F>
  auto write( F&& f ) { return access( std::forward<F>(f), *this); }
  template<class F>
  auto read( F&& f ) const { return access( std::forward<F>(f), *this); }

  template<class F, class...Guarded>
  friend auto access( F&& f, Guarded&&...guardeds );
private:
  T t;
  std::shared_timed_mutex m;
};

其中access占用任意数量的mutex_guarded并按顺序正确锁定它们,然后传递t传递的包裹f

这允许:

c.read( [&](auto&& c){
  std::copy_if( c.begin(), c.end(), std::back_inserter(dest),
  [&](auto&& e){
    this->ShouldCopy(e);
  });
} );

至少使互斥量使用粗俗。同样,如果不通过读取或写入功能,则无法访问数据,因此所有访问都具有互斥锁。但是在这里,我们至少可以进行多重锁定,并且可以使用代码来搜索互斥锁。

它仍然存在死锁风险,但总的来说,基于互斥的结构都存在这个问题。